hitl_tester.test_cases.cyber_6t.fingerprint
| Test | CAN Profiling |
|---|---|
| GitHub Issue(s) | turnaroundfactor/BMS-HW-Test#268 |
| Description | Tests for generating a 6T profile. |
(c) 2020-2024 TurnAround Factor, Inc.
#
CUI DISTRIBUTION CONTROL
Controlled by: DLA J68 R&D SBIP
CUI Category: Small Business Research and Technology
Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
POC: GOV SBIP Program Manager Denise Price, 571-767-0111
Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
Fort Belvoir, VA 22060-6221
#
SBIR DATA RIGHTS
Contract No.:SP4701-23-C-0083
Contractor Name: TurnAround Factor, Inc.
Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
Expiration of SBIR Data Rights Period: September 24, 2029
The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
the markings.
Used in these test plans:
- compare ⠀⠀⠀(cyber_6t/compare.plan)
- fingerprint ⠀⠀⠀(cyber_6t/fingerprint.plan)
Example Command (warning: test plan may run other test cases):
./hitl_tester.py compare -DBATTERY_CHANNEL=can0 -DHARD_RESET=False -DPROFILE="" -DFULL_MEMORY_TESTS=False -DLOG_PATH=/opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/site-packages/hitl_tester/logs
1""" 2| Test | CAN Profiling | 3| :------------------- | :----------------------------------------------------------- | 4| GitHub Issue(s) | turnaroundfactor/BMS-HW-Test#268 | 5| Description | Tests for generating a 6T profile. | 6 7# (c) 2020-2024 TurnAround Factor, Inc. 8# 9# CUI DISTRIBUTION CONTROL 10# Controlled by: DLA J68 R&D SBIP 11# CUI Category: Small Business Research and Technology 12# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS 13# POC: GOV SBIP Program Manager Denise Price, 571-767-0111 14# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the 15# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that 16# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests 17# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317, 18# Fort Belvoir, VA 22060-6221 19# 20# SBIR DATA RIGHTS 21# Contract No.:SP4701-23-C-0083 22# Contractor Name: TurnAround Factor, Inc. 23# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005 24# Expiration of SBIR Data Rights Period: September 24, 2029 25# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer 26# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights 27# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause 28# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any 29# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce 30# the markings. 31""" 32 33from __future__ import annotations 34 35import itertools 36import json 37import math 38import re 39import time 40from contextlib import suppress 41from enum import Enum, IntEnum 42from pathlib import Path 43from types import SimpleNamespace 44from typing import Literal, Any 45 46import pytest # pylint: disable=wrong-import-order 47from hitl_tester.modules.cyber_6t import charger, spn_types 48from hitl_tester.modules.cyber_6t.canbus import CANBus, CANFrame 49from hitl_tester.modules.cyber_6t.j1939da import PGN 50 51from hitl_tester.modules import properties 52from hitl_tester.modules.logger import logger 53 54BATTERY_CHANNEL = "can0" 55"""Channel identification. Expected type is backend dependent.""" 56 57HARD_RESET = False 58"""Whether to use a soft or hard reset before each test.""" 59 60PROFILE = "" 61"""The name of the profile to compare against. If this is not provided, a profile json will be generated.""" 62 63FULL_MEMORY_TESTS = False 64"""Short or long memory tests.""" 65 66LOG_PATH = Path(__file__).parent.parent.parent.resolve() / "logs" 67"""Path for profile logs.""" 68 69properties.apply() # Allow modifying the above globals 70 71_GENERATED_PROFILE = SimpleNamespace() 72"""Holds the results of each test.""" 73 74_COMPARISON_PROFILE = SimpleNamespace() 75"""The profile to compare results too.""" 76 77 78class ManufacturerID(IntEnum): 79 """Enum to hold Manufacturer IDs.""" 80 81 SAFT = 269 # NOTE: unused 82 BRENTRONICS = 822 83 84 85class Modes(float, Enum): 86 """Enum for Command modes""" 87 88 ERASE = 0 89 READ = 1 90 WRITE = 2 91 BOOT = 6 92 EDCP = 7 93 94 95class BadStates(Enum): 96 """Enum for Bad Test Response States""" 97 98 NO_RESPONSE = 0 99 WRONG_PGN = 1 100 NACK = 2 101 WRONG_PACKETS = 3 102 INVALID_RESPONSE = 4 103 SKIPPED_TEST = 5 104 105 106class Errors: 107 """Holds different error messages""" 108 109 @staticmethod 110 def timeout() -> None: 111 """Prints message when connection times out""" 112 message = "Could not locate 6T" 113 logger.write_critical_to_report(message) 114 pytest.exit(message) 115 116 @staticmethod 117 def unexpected_packet(expected: str, frame: CANFrame) -> None: 118 """Prints message when unexpected packet is received""" 119 logger.write_warning_to_report(f"Expected {expected}, got {frame.pgn.short_name}") 120 121 @staticmethod 122 def no_packet(expected: str) -> None: 123 """Prints message when no packet is received""" 124 logger.write_warning_to_report(f"Expected {expected}, got None") 125 126 127# FIXME(JA): change issue to the prop issue / name issue 128 129 130def cmp( 131 a: float | str, 132 sign: Literal["<", "<=", ">", ">=", "=="], 133 b: float | str, 134 unit_a: str = "", 135 unit_b: str = "", 136 form: str = "", 137) -> str: 138 """Generate a formatted string based on a comparison.""" 139 if not unit_b: 140 unit_b = unit_a 141 142 if isinstance(a, str) or isinstance(b, str): 143 return f"{a:{form}}{unit_a} {('≠', '=')[a == b]} {b:{form}}{unit_b}" 144 145 sign_str = { 146 "<": ("≮", "<")[a < b], 147 "<=": ("≰", "≤")[a <= b], 148 ">": ("≯", ">")[a > b], 149 ">=": ("≱", "≥")[a >= b], 150 "==": ("≠", "=")[a == b], 151 } 152 153 return f"{a:{form}}{unit_a} {sign_str[sign]} {b:{form}}{unit_b}" 154 155 156@pytest.fixture(scope="session", autouse=True) 157def generate_profile(): 158 """Generate a json object for the profile after all tests complete.""" 159 global _COMPARISON_PROFILE 160 161 if PROFILE: 162 _COMPARISON_PROFILE = SimpleNamespace(**json.loads(Path(PROFILE).read_text(encoding="UTF-8"))) 163 164 yield # Begin session 165 166 if not PROFILE: 167 try: 168 manufacturer_name = "".join(filter(str.isalnum, _GENERATED_PROFILE.manufacturer_name)) 169 battery_id = f"{_GENERATED_PROFILE.id:06X}" 170 except AttributeError: 171 logger.write_error_to_report("Could not write profile: No name or ID") 172 else: 173 profile_path = (Path(LOG_PATH) / "profile") / f"{manufacturer_name}_6T_{battery_id}.json" 174 profile_path.parent.mkdir(parents=True, exist_ok=True) 175 profile_path.write_text(json.dumps(_GENERATED_PROFILE, default=vars, indent=4), encoding="UTF-8") 176 logger.write_info_to_report(f"New Profile: {profile_path.absolute()}") 177 178 179@pytest.fixture(scope="class", autouse=True) 180def reset_test_environment(): 181 """Before each test class, reset the 6T.""" 182 183 try: 184 power_cycle_frame = CANFrame( 185 destination_address=_GENERATED_PROFILE.address, 186 pgn=PGN["PropA"], 187 data=[0, 0, 1, 1, 1, not HARD_RESET, 1, 3, 0, -1], 188 ) 189 maintenance_mode_frame = CANFrame( 190 destination_address=_GENERATED_PROFILE.address, 191 pgn=PGN["PropA"], 192 data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1], 193 ) 194 factory_reset_frame = CANFrame( 195 destination_address=_GENERATED_PROFILE.address, 196 pgn=PGN["PropA"], 197 data=[1, 0, 0, -1, 3, 0xF, 3, 0, 0x1F, -1], 198 ) 199 with CANBus(BATTERY_CHANNEL) as bus: 200 logger.write_info_to_report("Power-Cycling 6T") 201 bus.process_call(power_cycle_frame) 202 time.sleep(10) 203 logger.write_info_to_report("Entering maintenance mode") 204 bus.process_call(maintenance_mode_frame) 205 logger.write_info_to_report("Factory resetting 6T") 206 bus.process_call(factory_reset_frame) 207 time.sleep(10) 208 except AttributeError: # Battery has not yet been found 209 return 210 211 212@pytest.fixture(autouse=True) 213def check_mode(request): 214 """Skip comparison tests and output a profile if no comparison file is provided.""" 215 if not PROFILE and request.node.get_closest_marker("profile") is None: 216 pytest.skip('Mode is "Profile Generation".') 217 218 219class TestLocate6T: 220 """Scan for 6T battery.""" 221 222 MAX_ATTEMPTS = 12 # 2 minutes worth of attempts 223 224 @pytest.mark.profile 225 def test_profile(self) -> None: 226 """ 227 | Description | Scan the bus for devices | 228 | :------------------- | :--------------------------------------------------------------- | 229 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 230 | Instructions | 1. Request the names of everyone on the bus </br>\ 231 2. Use the response data for all communication | 232 | Estimated Duration | 1 second | 233 """ 234 name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 235 with CANBus(BATTERY_CHANNEL) as bus: 236 for attempt in range(self.MAX_ATTEMPTS): 237 with suppress(IndexError): 238 if name_frame := bus.process_call(name_request): 239 # Save responses to profile 240 _GENERATED_PROFILE.id = int(name_frame.data[0]) 241 _GENERATED_PROFILE.manufacturer_code = name_frame.data[1] 242 if (mfg_name := spn_types.manufacturer_id(name_frame.data[1])) == "Reserved": 243 mfg_name = f"Unknown ({name_frame.data[1]})" 244 _GENERATED_PROFILE.manufacturer_name = mfg_name 245 _GENERATED_PROFILE.address = name_frame.source_address 246 break 247 logger.write_warning_to_html_report(f"Failed to locate 6T. Retrying, attempt {attempt + 1}") 248 time.sleep(10) 249 else: 250 message = "Could not locate 6T" 251 logger.write_warning_to_html_report(message) 252 pytest.exit(message) 253 254 # Log results 255 printable_id = "".join( 256 chr(i) if chr(i).isprintable() else "." for i in _GENERATED_PROFILE.id.to_bytes(3, byteorder="big") 257 ) 258 logger.write_result_to_html_report( 259 f"Found {_GENERATED_PROFILE.manufacturer_name} 6T at address {_GENERATED_PROFILE.address} " 260 f"(ID: {_GENERATED_PROFILE.id:06X}, ASCII ID: {printable_id})" 261 ) 262 263 264class TestProprietaryCommands: 265 """Scan for proprietary commands""" 266 267 @pytest.mark.profile 268 def test_profile(self) -> None: 269 """ 270 | Description | Scan for proprietary commands | 271 | :------------------- | :--------------------------------------------------------------- | 272 | GitHub Issue | turnaroundfactor/BMS-HW-Test#293 | 273 | Instructions | 1. Request data from all 512 proprietary commands </br>\ 274 2. Log any responses | 275 | Estimated Duration | 47 seconds | 276 | Note | Proprietary commands are unique to each battery type or \ 277 manufacturer, which makes them useful for fingerprinting. \ 278 Especially if undocumented. | 279 """ 280 ack_responses: dict[int, CANFrame] = {} # address: response 281 proprietary_request = CANFrame(destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[0]) 282 283 with CANBus(BATTERY_CHANNEL) as bus: 284 # Check all proprietary address ranges (0xEF00-FF00) and (0xFF00, 0x10000) 285 for address in itertools.chain(range(0xEF00, 0xF000), range(0xFF00, 0x10000)): 286 proprietary_request.data = [address] 287 if response_frame := bus.process_call(proprietary_request): # Log if response is not NACK 288 if not (response_frame.pgn.id == PGN["Acknowledgement"].id and response_frame.data[0] == 1): 289 ack_responses[address] = response_frame 290 logger.write_info_to_report(f"Got response at address {address:04X}: {response_frame}") 291 292 # Check conformity to MIL-PRF commands (EF00, FF00-08) 293 # TODO(JA): Also check format (or should that be a separate test?) 294 milprf_commands = set(range(0xFF00, 0xFF09)) 295 if milprf := milprf_commands - set(ack_responses): 296 logger.write_result_to_html_report(f"Missing MIL-PRF commands: {', '.join(map(str, milprf))}") 297 298 # Save responses to profile 299 logger.write_result_to_html_report( 300 f"Found {len(ack_responses)} commands at: {', '.join(map(lambda i: f'{i:X}', ack_responses))}" 301 ) 302 _GENERATED_PROFILE.proprietary_commands = ack_responses 303 304 def test_comparison(self) -> None: 305 """ 306 | Description | Compare proprietary commands | 307 | :------------------- | :--------------------------------------------------------------- | 308 | GitHub Issue | turnaroundfactor/BMS-HW-Test#293 | 309 | Instructions | 1. Confirm no new commands exit and no commands are missing </br>\ 310 2. Check contents of MIL-PRF range is valid | 311 | Pass / Fail Criteria | Pass if we found all commands, and saw no new or malformed ones | 312 | Estimated Duration | 1 second | 313 """ 314 315 comparison_commands = set(map(int, _COMPARISON_PROFILE.proprietary_commands)) # Converted to str by json 316 missing_commands = comparison_commands - set(_GENERATED_PROFILE.proprietary_commands) 317 new_commands = set(_GENERATED_PROFILE.proprietary_commands) - comparison_commands 318 319 # Check results 320 logger.write_result_to_html_report(f"Missing: {len(missing_commands)}, New: {len(new_commands)}") 321 if missing_commands or new_commands: 322 logger.write_warning_to_html_report(f"Missing commands: {', '.join(map(str, missing_commands))}") 323 logger.write_warning_to_html_report(f"New Commands: {', '.join(map(str, new_commands))}") 324 logger.write_failure_to_html_report("Missing or new commands present.") 325 pytest.fail("Missing or new commands present.") 326 327 328class TestNameInformation: 329 """Check Unique ID (Identity Number) field in Address Claimed message""" 330 331 @pytest.mark.profile 332 def test_profile(self) -> None: 333 """ 334 | Description | Confirm information in NAME (address claimed) matches \ 335 expected value. | 336 | :------------------- | :--------------------------------------------------------------- | 337 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 338 | Instructions | 1. Request Address Claimed data </br>\ 339 2. Log returned values </br>\ 340 3. Validate if values meet spec requirements | 341 | Estimated Duration | 21 seconds | 342 """ 343 344 name_values = {} 345 346 name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 347 with CANBus(BATTERY_CHANNEL) as bus: 348 if name_frame := bus.process_call(name_request): 349 name_values["identity_number"] = int(name_frame.data[0]) 350 351 if name_values["identity_number"] != _GENERATED_PROFILE.id: 352 message = f"Identity Number {name_values} does not match Profile ID: {_GENERATED_PROFILE.id}" 353 logger.write_warning_to_html_report(message) 354 355 name_values["manufacturer_code"] = name_frame.data[1] 356 name_values["ecu_instance"] = name_frame.data[2] 357 name_values["function_instance"] = name_frame.data[3] 358 name_values["function"] = name_frame.data[4] 359 name_values["reserved"] = name_frame.data[5] 360 name_values["vehicle_system"] = name_frame.data[6] 361 name_values["vehicle_system_instance"] = name_frame.data[7] 362 name_values["industry_group"] = name_frame.data[8] 363 name_values["arbitrary_address_capable"] = name_frame.data[9] 364 365 for spn, elem in zip(PGN["Address Claimed"].data_field, name_frame.data): 366 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 367 logger.write_info_to_report(f"{spn.name}: {elem} {description}") 368 369 if spn.name in ("Manufacturer Code", "Reserved"): 370 continue 371 372 high_range = 0 373 if spn.name == "Identity Number": 374 high_range = 2097151 375 376 if spn.name == "ECU Instance": 377 high_range = 7 378 379 if spn.name == "Function Instance": 380 high_range = 31 381 382 if spn.name == "Function": 383 high_range = 254 384 385 if spn.name == "Vehicle System Instance": 386 high_range = 15 387 388 if spn.name == "Industry Group": 389 high_range = 7 390 391 if spn.name == "Arbitrary Address Capable": 392 high_range = 1 393 394 if spn.name == "Vehicle System": 395 if ( 396 name_values["vehicle_system"] != 127 397 and name_values["vehicle_system"] != 0 398 and name_values["vehicle_system"] != name_values["vehicle_system_instance"] 399 ): 400 logger.write_warning_to_html_report( 401 f"Vehicle System {elem} is not 127, 0, or the same value as " f"Vehicle System Instance" 402 ) 403 _GENERATED_PROFILE.name_violation = "invalid" 404 else: 405 _GENERATED_PROFILE.name_violation = "valid" 406 407 else: 408 if not 0 <= elem <= high_range: 409 logger.write_warning_to_html_report( 410 f"{spn.name}: {cmp(elem, '>=', 0)} and " f"{cmp(elem, '<=', high_range)}" 411 ) 412 _GENERATED_PROFILE.name_violation = "invalid" 413 else: 414 _GENERATED_PROFILE.name_violation = "valid" 415 416 _GENERATED_PROFILE.name_properties = name_values 417 418 else: 419 message = f"No response to PGN {PGN['Address Claimed', [32]].id} (Address Claimed)" 420 logger.write_warning_to_html_report(message) 421 _GENERATED_PROFILE.name_field = BadStates.NO_RESPONSE.name 422 _GENERATED_PROFILE.name_violation = "invalid" 423 return 424 425 logger.write_result_to_html_report( 426 f"Found Identity Number: {_GENERATED_PROFILE.id} (0x{_GENERATED_PROFILE.id:06x}) " 427 f"for {_GENERATED_PROFILE.manufacturer_name} 6T at address {_GENERATED_PROFILE.address}" 428 ) 429 430 def test_comparison(self) -> None: 431 """ 432 | Description | Compare identity numbers | 433 | :------------------- | :--------------------------------------------------------------- | 434 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 435 | Instructions | 1. Confirm identity numbers are unique for each battery | 436 | Pass / Fail Criteria | Pass if identity numbers are unique for separate batteries | 437 | Estimated Duration | 1 second | 438 """ 439 failed_test = False 440 if not hasattr(_GENERATED_PROFILE, "id") or not hasattr(_COMPARISON_PROFILE, "id"): 441 logger.write_result_to_html_report("Nothing to compare. The profile test may have failed or been skipped") 442 pytest.skip("Nothing to compare. The profile test may have failed or been skipped") 443 444 logger.write_result_to_html_report( 445 f"Generated ID: {_GENERATED_PROFILE.id}, " f"Comparison ID: {_COMPARISON_PROFILE.id}" 446 ) 447 if _GENERATED_PROFILE.id == _COMPARISON_PROFILE.id: 448 logger.write_warning_to_html_report("Generated IDs are not unique values between batteries.") 449 else: 450 logger.write_result_to_html_report("Generated IDs are unique values") 451 452 if not hasattr(_GENERATED_PROFILE, "name_violation") or not hasattr(_COMPARISON_PROFILE, "name_violation"): 453 logger.write_warning_to_html_report("Missing specification information") 454 else: 455 comparison = cmp(_GENERATED_PROFILE.name_violation, "==", _COMPARISON_PROFILE.name_violation) 456 if _GENERATED_PROFILE.name_violation != _COMPARISON_PROFILE.name_violation: 457 logger.write_failure_to_html_report(f"Specification behavior mismatched: {comparison}") 458 failed_test = True 459 else: 460 logger.write_result_to_html_report(f"Specification behavior matched: {comparison}") 461 462 if failed_test: 463 message = "Comparisons did not meet expectations" 464 logger.write_failure_to_html_report(message) 465 pytest.fail(message) 466 467 468class TestSoftwareIdentificationInformation: 469 """Retrieve & Validate Software Identification Information""" 470 471 @pytest.mark.profile 472 def test_software_identification_information(self) -> None: 473 """ 474 | Description | Retrieve Software Identification | 475 | :------------------- | :--------------------------------------------------------------- | 476 | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 477 | Instructions | 1. Request Software Identification </br>\ 478 2. Validate Software Identification </br>\ 479 3. Log Response | 480 | Estimated Duration | 21 seconds | 481 """ 482 483 soft_request = CANFrame(pgn=PGN["RQST"], data=[0xFEDA]) 484 soft_data = [] 485 with CANBus(BATTERY_CHANNEL) as bus: 486 if soft_frame := bus.process_call(soft_request): 487 # Complete RTS 488 if len(soft_frame.data) < 3: 489 message = ( 490 f"Unexpected byte response from PGN " 491 f"{PGN['Software Identification', [32]].id} (Software Identification)" 492 ) 493 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 494 logger.write_warning_to_html_report(message) 495 _GENERATED_PROFILE.software_identification_specification = "invalid" 496 return 497 expected_bytes = int(soft_frame.data[1]) 498 expected_packets = 0 499 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.BRENTRONICS: 500 expected_packets = int(soft_frame.data[2] + 1) 501 else: 502 expected_packets = int(soft_frame.data[2]) 503 504 rts_pgn_id = int(soft_frame.data[-1]) 505 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 506 cts_request.data[1] = expected_packets 507 cts_request.data[-1] = rts_pgn_id 508 509 # Send CTS 510 bus.send_message(cts_request.message()) 511 512 # Read & Store data from packets 513 for i in range(expected_packets): 514 data_frame = bus.read_frame() 515 if data_frame.pgn.id != 0xEB00: 516 message = f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id}" 517 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 518 _GENERATED_PROFILE.software_identification_specification = "invalid" 519 logger.write_warning_to_html_report(message) 520 return 521 soft_data.append(hex(int(data_frame.data[1]))) 522 523 if len(soft_data) != expected_packets: 524 message = f"Expected {expected_packets} packets, got {len(soft_data)}" 525 logger.write_warning_to_html_report(message) 526 _GENERATED_PROFILE.software_identification_specification = "invalid" 527 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 528 return 529 530 # Send acknowledgement frame 531 end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 532 end_acknowledge_frame.data[1] = expected_bytes 533 end_acknowledge_frame.data[2] = expected_packets 534 end_acknowledge_frame.data[-1] = rts_pgn_id 535 536 bus.send_message(end_acknowledge_frame.message()) 537 else: 538 message = f"No response for PGN {PGN['Software Identification', [32]].id} (Software Identification)" 539 logger.write_warning_to_html_report(message) 540 _GENERATED_PROFILE.software_identification_information = BadStates.NO_RESPONSE.name 541 _GENERATED_PROFILE.software_identification_specification = "invalid" 542 return 543 544 hex_string = "0x" 545 add_to_data = False 546 finish_adding = False 547 548 # Find Software Identification Number 549 for element in soft_data: 550 n = len(element) - 1 551 while n >= 1: 552 if add_to_data: 553 if element[n - 1 : n + 1] == "2a": 554 finish_adding = True 555 break 556 if element[n - 1 : n + 1] != "0x": 557 hex_string += element[n - 1 : n + 1] 558 n -= 2 559 else: 560 n -= 2 561 else: 562 if element[n - 3 : n + 1] == "2a31" or element[n - 3 : n + 1] == "2a01": 563 add_to_data = True 564 n -= 4 565 else: 566 n -= 4 567 if finish_adding: 568 break 569 570 if len(hex_string) <= 2: 571 logger.write_warning_to_html_report( 572 f"Software Identification: {hex_string} was not found in expected format" 573 ) 574 _GENERATED_PROFILE.software_identification_information = hex_string 575 _GENERATED_PROFILE.software_identification_specification = "invalid" 576 return 577 578 software_identification = spn_types.ascii_map(int(hex_string, 16)) 579 580 if re.search(r"[0-9]{2}\.[0-9]{2}\.[a-z]{2}\.[a-z]{2,}", software_identification) is None: 581 message = f"Software Identification: {software_identification} was not in expected format: MM.II.mm.aa.ee" 582 logger.write_warning_to_html_report(message) 583 _GENERATED_PROFILE.software_identification_specification = "invalid" 584 else: 585 _GENERATED_PROFILE.software_identification_specification = "valid" 586 587 _GENERATED_PROFILE.software_identification = software_identification 588 589 logger.write_result_to_html_report(f"Software Identification: {software_identification}") 590 591 def test_comparison(self) -> None: 592 """ 593 | Description | Compare Software Identification values | 594 | :------------------- | :--------------------------------------------------------------- | 595 | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 596 | Instructions | 1. Confirm software versions are the same for each profile | 597 | Pass / Fail Criteria | Pass if identity numbers match for each profile | 598 | Estimated Duration | 1 second | 599 """ 600 failed_test = False 601 if not hasattr(_GENERATED_PROFILE, "software_identification") or not hasattr( 602 _COMPARISON_PROFILE, "software_identification" 603 ): 604 message = "Nothing to compare. The profile test may have failed or been skipped" 605 logger.write_result_to_html_report(message) 606 pytest.skip(message) 607 608 compare = cmp(_GENERATED_PROFILE.software_identification, "==", _COMPARISON_PROFILE.software_identification) 609 if _GENERATED_PROFILE.software_identification != _COMPARISON_PROFILE.software_identification: 610 logger.write_failure_to_html_report(f"Software Identification: {compare}") 611 failed_test = True 612 else: 613 logger.write_result_to_html_report(f"Software Identification: {compare}") 614 615 if not hasattr(_GENERATED_PROFILE, "software_identification_specification") or not hasattr( 616 _COMPARISON_PROFILE, "software_identification_specification" 617 ): 618 logger.write_warning_to_html_report("Missing specification information") 619 else: 620 comparison = cmp( 621 _GENERATED_PROFILE.software_identification_specification, 622 "==", 623 _COMPARISON_PROFILE.software_identification_specification, 624 ) 625 if ( 626 _GENERATED_PROFILE.software_identification_specification 627 != _COMPARISON_PROFILE.software_identification_specification 628 ): 629 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 630 failed_test = True 631 else: 632 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 633 634 if failed_test: 635 message = "Comparisons did not match in profiles" 636 logger.write_failure_to_html_report(message) 637 pytest.fail(message) 638 639 640class TestBatteryRegulationInformation: 641 """Retrieve and validate battery regulation information""" 642 643 @pytest.mark.profile 644 def test_profile(self) -> None: 645 """ 646 | Description | Obtain Battery Regulation Information 1 & 2 | 647 | :------------------- | :--------------------------------------------------------------- | 648 | GitHub Issue | turnaroundfactor/BMS-HW-Test#392 | 649 | Instructions | 1. Request Battery Regulation Information 1 </br>\ 650 2. Validate Information </br>\ 651 3. Log Response | 652 | Estimated Duration | 22 seconds | 653 """ 654 655 battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03]) 656 battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04]) 657 658 # Obtain Battery Regulation One Information 659 with CANBus(BATTERY_CHANNEL) as bus: 660 if battery_frame := bus.process_call(battery_regulation_one_request): 661 logger.write_result_to_html_report( 662 "<span style='font-weight: bold'>Battery Regulation Information 1 </span>" 663 ) 664 if battery_frame.pgn.id != PGN["PropB_03", [32]].id: 665 message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}" 666 logger.write_warning_to_html_report(message) 667 _GENERATED_PROFILE.battery_regulation_one = BadStates.WRONG_PGN.name 668 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 669 else: 670 _GENERATED_PROFILE.battery_regulation_one = { 671 "battery_voltage": battery_frame.data[0], 672 "open_circuit_voltage": battery_frame.data[1], 673 "battery_current": battery_frame.data[2], 674 "maximum_charge_current": battery_frame.data[3], 675 } 676 677 for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data): 678 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 679 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 680 681 high_range = 3212.75 682 low_range = 0.0 683 if spn.name == "Battery Current": 684 high_range = 1600.00 685 low_range = -1600.00 686 687 if not low_range <= elem <= high_range: 688 logger.write_warning_to_html_report( 689 f"{spn.name}: {cmp(elem, '>=', low_range)} and " f"{cmp(elem, '<=', high_range)}" 690 ) 691 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 692 693 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one_specification"): 694 _GENERATED_PROFILE.battery_regulation_one_specification = "valid" 695 696 else: 697 message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)" 698 logger.write_warning_to_html_report(message) 699 _GENERATED_PROFILE.battery_regulation_one = BadStates.NO_RESPONSE.name 700 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 701 702 # Obtain Battery Regulation Two Information 703 with CANBus(BATTERY_CHANNEL) as bus: 704 if battery_frame_two := bus.process_call(battery_regulation_two_request): 705 logger.write_result_to_html_report( 706 "<span style='font-weight: bold'>Battery Regulation Information 2</span>" 707 ) 708 709 if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id: 710 message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}" 711 logger.write_warning_to_html_report(message) 712 _GENERATED_PROFILE.battery_regulation_two = BadStates.NO_RESPONSE.name 713 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 714 else: 715 _GENERATED_PROFILE.battery_regulation_two = { 716 "contractor_state": battery_frame_two.data[0], 717 "charge_capability_state": battery_frame_two.data[1], 718 "bus_voltage_request": battery_frame_two.data[3], 719 "transportability_soc": battery_frame_two.data[4], 720 } 721 722 for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data): 723 724 if spn.name == "Reserved": 725 continue 726 727 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 728 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 729 730 high_range = 3 731 if spn.name == "Bus Voltage Request": 732 high_range = 3212.75 733 734 if spn.name == "Transportability SOC": 735 high_range = 100 736 737 if not 0 <= elem <= high_range: 738 logger.write_warning_to_html_report( 739 f"{spn.name}: {cmp(elem, '>=', 0)} and {cmp(elem, '<=', high_range)}" 740 ) 741 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 742 743 if not hasattr(_GENERATED_PROFILE, "battery_regulation_two_specification"): 744 _GENERATED_PROFILE.battery_regulation_two_specification = "valid" 745 746 else: 747 message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)" 748 logger.write_warning_to_html_report(message) 749 _GENERATED_PROFILE.battery_regulation_two = BadStates.NO_RESPONSE.name 750 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 751 752 # def test_comparison(self) -> None: 753 # # TODO: FIX 754 # """ 755 # | Description | Compare Battery Regulation values | 756 # | :------------------- | :--------------------------------------------------------------- | 757 # | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 758 # | Instructions | 1. Confirm Battery Regulation One profiles match </br>\ 759 # 2. Confirm Battery Regulation Two profiles match | 760 # | Pass / Fail Criteria | Pass if Battery Regulation values match for each profile | 761 # | Estimated Duration | 1 second | 762 # """ 763 # 764 # def loop_profiles(generated, comparison) -> list[str]: 765 # fail_list = [] 766 # for key, value in generated.items(): 767 # key_text = key.replace("_", " ").title() 768 # message = cmp(value, "==", comparison[key]) 769 # if comparison[key] != value: 770 # logger.write_warning_to_html_report(f"{key_text}: {message}") 771 # fail_list.append(key_text) 772 # else: 773 # logger.write_result_to_html_report(f"{key_text}: {message}") 774 # 775 # return fail_list 776 # 777 # test_failed = False 778 # reg_one_list: list[str] = [] 779 # reg_two_list: list[str] = [] 780 # if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") or not hasattr( 781 # _COMPARISON_PROFILE, "battery_regulation_one" 782 # ): 783 # message = "Nothing to compare. The profile test may have failed or been skipped" 784 # logger.write_result_to_html_report(message) 785 # pytest.skip(message) 786 # 787 # if not hasattr(_GENERATED_PROFILE, "battery_regulation_two") or not hasattr( 788 # _COMPARISON_PROFILE, "battery_regulation_two" 789 # ): 790 # message = "Nothing to compare. The profile test may have failed or been skipped" 791 # logger.write_result_to_html_report(message) 792 # pytest.skip(message) 793 # 794 # logger.write_result_to_html_report( 795 # "<span style='font-weight: bold'>Battery Regulation One Comparisons</span>" 796 # ) 797 # if isinstance(_GENERATED_PROFILE.battery_regulation_one, str) or isinstance( 798 # _COMPARISON_PROFILE.battery_regulation_one, str 799 # ): 800 # if not isinstance( 801 # _GENERATED_PROFILE.battery_regulation_one, type(_COMPARISON_PROFILE.battery_regulation_one) 802 # ): 803 # message = "Unable to compare profiles, Battery Regulation One types did not match" 804 # logger.write_warning_to_html_report(message) 805 # pytest.fail(message) 806 # 807 # comparison_text = cmp( 808 # _GENERATED_PROFILE.battery_regulation_one, "==", _COMPARISON_PROFILE.battery_regulation_one 809 # ) 810 # 811 # if _GENERATED_PROFILE.battery_regulation_one != _COMPARISON_PROFILE.battery_regulation_one: 812 # message = f"Battery Regulation One did not match: {comparison_text}" 813 # logger.write_warning_to_html_report(message) 814 # test_failed = True 815 # else: 816 # reg_one_list = loop_profiles( 817 # _GENERATED_PROFILE.battery_regulation_one, _COMPARISON_PROFILE.battery_regulation_one 818 # ) 819 # 820 # logger.write_result_to_html_report( 821 # "<span style='font-weight: bold'>Battery Regulation Two Comparisons</span>" 822 # ) 823 # if isinstance(_GENERATED_PROFILE.battery_regulation_two, str) or isinstance( 824 # _COMPARISON_PROFILE.battery_regulation_two, str 825 # ): 826 # if not isinstance( 827 # _GENERATED_PROFILE.battery_regulation_two, type(_COMPARISON_PROFILE.battery_regulation_two) 828 # ): 829 # message = "Unable to compare profiles, Battery Regulation One types did not match" 830 # logger.write_warning_to_html_report(message) 831 # pytest.fail(message) 832 # 833 # comparison_text = cmp( 834 # _GENERATED_PROFILE.battery_regulation_two, "==", _COMPARISON_PROFILE.battery_regulation_two 835 # ) 836 # 837 # if _GENERATED_PROFILE.battery_regulation_two != _COMPARISON_PROFILE.battery_regulation_two: 838 # message = f"Battery Regulation Two did not match: {comparison_text}" 839 # logger.write_warning_to_html_report(message) 840 # test_failed = True 841 # else: 842 # reg_two_list = loop_profiles( 843 # _GENERATED_PROFILE.battery_regulation_two, _COMPARISON_PROFILE.battery_regulation_two 844 # ) 845 # 846 # if test_failed: 847 # message = "Battery Regulation values did not match" 848 # logger.write_failure_to_html_report(message) 849 # pytest.fail(message) 850 # 851 # fail_categories = reg_one_list + reg_two_list 852 # 853 # if len(fail_categories) > 0: 854 # categories = "data values" if len(fail_categories) > 1 else "data value" 855 # message = f"{len(fail_categories)} {categories} failed profile comparisons: {', '.join(fail_categories)}" 856 # logger.write_failure_to_html_report(message) 857 # pytest.fail(message) 858 # else: 859 # logger.write_result_to_html_report("Battery Regulation 1 and 2 profiles match") 860 861 862class TestPowerSupply: 863 """Retrieve and validate battery regulation information""" 864 865 @pytest.mark.profile 866 def test_charge_power_supply(self) -> None: 867 """ 868 | Description | Check Battery Information after Charge | 869 | :------------------- | :--------------------------------------------------------------- | 870 | GitHub Issue | turnaroundfactor/BMS-HW-Test#597 | 871 | Instructions | 1. Request Battery State of Charge </br>\ 872 2. Charge battery at 0.1A for 1 minute </br>\ 873 3. Check & save Battery Regulation 1 & 2 information </br>\ 874 4. Log Response | 875 | Estimated Duration | 2 minutes | 876 """ 877 878 battery_calculation_one = CANFrame(pgn=PGN["RQST"], data=[PGN["PropB_02", [32]].id]) 879 880 with CANBus(BATTERY_CHANNEL) as bus: 881 if battery_calculation_response := bus.process_call(battery_calculation_one): 882 if battery_calculation_response.pgn.id != PGN["PropB_02", [32]].id: 883 message = ( 884 f"Expected PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name}) " 885 f" but received PGN {battery_calculation_response.pgn.id}" 886 ) 887 logger.write_warning_to_html_report(message) 888 _GENERATED_PROFILE.test_power_supply = BadStates.WRONG_PGN.name 889 pytest.skip(message) 890 891 for spn, elem in zip(PGN["PropB_02"].data_field, battery_calculation_response.data): 892 low_range = 0 893 high_range = 95 894 if spn.name == "Battery State of Charge": 895 comparison = f"{cmp(elem, '>=', low_range, '%')} " f"and {cmp(elem, '<=', high_range, '%' )}" 896 897 if not low_range <= elem <= high_range: 898 message = ( 899 f"Skipping Test: {spn.name} is {elem}%, " 900 f"which is not within expected range: {comparison}" 901 ) 902 logger.write_warning_to_html_report(message) 903 _GENERATED_PROFILE.test_power_supply = BadStates.SKIPPED_TEST.name 904 pytest.skip(message) 905 else: 906 message = f"{spn.name} is {elem}%" 907 logger.write_result_to_html_report(message) 908 break 909 else: 910 message = f"Did not receive response for PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name})" 911 logger.write_warning_to_html_report(message) 912 _GENERATED_PROFILE.test_power_supply = BadStates.NO_RESPONSE.name 913 pytest.skip(message) 914 915 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") and not hasattr( 916 _GENERATED_PROFILE, "battery_regulation_two" 917 ): 918 default_b_one = self.get_battery_regulation_one() 919 default_b_two = self.get_battery_regulation_two() 920 else: 921 default_b_one = _GENERATED_PROFILE.battery_regulation_one 922 default_b_two = _GENERATED_PROFILE.battery_regulation_two 923 924 if isinstance(default_b_one, str) or isinstance(default_b_two, str): 925 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 926 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 927 pytest.skip("Missing valid Battery Regulation One and Two info") 928 929 logger.write_result_to_html_report("Charging battery") 930 try: 931 cyber_charger = charger.Charger() 932 except RuntimeError: 933 message = "Could not communicate with power supply" 934 logger.write_warning_to_html_report(message) 935 pytest.fail(message) 936 937 try: 938 with cyber_charger(0.1, float(default_b_two.get("bus_voltage_request", 0.0))): 939 logger.write_info_to_report("Battery Regulation One Values from Charge") 940 updated_b_one = self.get_battery_regulation_one() 941 logger.write_info_to_report("Battery Regulation Two Values from Charge") 942 updated_b_two = self.get_battery_regulation_two() 943 944 if isinstance(updated_b_one, str): 945 if updated_b_one == "No Current": 946 _GENERATED_PROFILE.test_charge_power_supply = "Current not detected" 947 raise RuntimeError 948 949 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 950 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 951 pytest.skip("Missing valid Battery Regulation One and Two info") 952 953 _GENERATED_PROFILE.charge_b_one = updated_b_one 954 _GENERATED_PROFILE.charge_b_two = updated_b_two 955 956 except RuntimeError: 957 if cyber_charger.resource: 958 if hasattr(cyber_charger.resource, "close"): 959 cyber_charger.resource.close() 960 message = "Could not communicate with power supply or current not detected" 961 logger.write_warning_to_html_report(message) 962 963 if not hasattr(_GENERATED_PROFILE, "test_charge_power_supply"): 964 _GENERATED_PROFILE.test_charge_power_supply = "Could not communicate" 965 pytest.fail(message) 966 967 except AttributeError: 968 message = "Could not communicate with power supply" 969 logger.write_warning_to_html_report(message) 970 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 971 pytest.fail(message) 972 973 def test_charge_comparison(self): 974 """ 975 | Description | Compare Power Supply Charge values | 976 | :------------------- | :--------------------------------------------------------------- | 977 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 978 | Instructions | 1. Check if charge Battery Current is within 10% of initial value| 979 | Pass / Fail Criteria | Pass if charged value is within 10% | 980 | Estimated Duration | 1 second | 981 """ 982 983 if hasattr(_GENERATED_PROFILE, "test_power_supply") or not hasattr(_GENERATED_PROFILE, "charge_b_one"): 984 message = "Nothing to compare. Profile test may have failed or been skipped" 985 logger.write_result_to_html_report(message) 986 pytest.skip(message) 987 988 if hasattr(_COMPARISON_PROFILE, "test_power_supply") or not hasattr(_COMPARISON_PROFILE, "charge_b_one"): 989 message = "Nothing to compare. Profile test may have failed or been skipped" 990 logger.write_result_to_html_report(message) 991 pytest.skip(message) 992 993 if isinstance(_GENERATED_PROFILE.charge_b_one, str) or isinstance(_COMPARISON_PROFILE.charge_b_one, str): 994 if not isinstance(_GENERATED_PROFILE.charge_b_one, type(_COMPARISON_PROFILE.charge_b_one)): 995 message = "Unable to compare profiles, types of Battery Regulation Information 1 did not match" 996 logger.write_failure_to_html_report(message) 997 pytest.fail(message) 998 999 comparison = cmp(_GENERATED_PROFILE.charge_b_one, "==", _COMPARISON_PROFILE.charge_b_one) 1000 1001 if _GENERATED_PROFILE.charge_b_one != _COMPARISON_PROFILE.charge_b_one: 1002 message = f"Battery Regulation Information 1 matched profiles: {comparison}" 1003 logger.write_failure_to_html_report(message) 1004 pytest.fail(message) 1005 1006 message = f"Battery Regulation Information 1 matched: {comparison}" 1007 logger.write_result_to_html_report(message) 1008 return 1009 1010 original_current = _GENERATED_PROFILE.charge_b_one.get("battery_current") 1011 comparison_current = _COMPARISON_PROFILE.charge_b_one.get("battery_current") 1012 1013 diff = abs(original_current - comparison_current) 1014 average = abs(original_current + comparison_current) / 2 1015 percentage = round((diff / average), 4) 1016 percent_closeness = 0.1 1017 1018 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 1019 1020 if not math.isclose(original_current, comparison_current, rel_tol=0.1): 1021 logger.write_warning_to_html_report( 1022 f"Battery Regulation Information 1 current percent difference: {comparison}" 1023 ) 1024 logger.write_failure_to_html_report("Current was not within expected range during charge") 1025 pytest.fail("Current was not within expected range during charge") 1026 1027 logger.write_result_to_html_report(f"Battery Regulation Information 1 current percent difference: {comparison}") 1028 logger.write_result_to_html_report("Current was within expected range during charge") 1029 1030 @pytest.mark.profile 1031 def test_discharge_power_supply(self) -> None: 1032 """ 1033 | Description | Check Battery Information after Discharge | 1034 | :------------------- | :--------------------------------------------------------------- | 1035 | GitHub Issue | turnaroundfactor/BMS-HW-Test#597 | 1036 | Instructions | 1. Request Battery State of Charge </br>\ 1037 2. Discharge battery at -0.1A for 1 minute </br>\ 1038 3. Check & save Battery Regulation 1 & 2 information </br>\ 1039 4. Log Response | 1040 | Estimated Duration | 2 minutes | 1041 """ 1042 1043 battery_calculation_one = CANFrame(pgn=PGN["RQST"], data=[PGN["PropB_02", [32]].id]) 1044 1045 with CANBus(BATTERY_CHANNEL) as bus: 1046 if battery_calculation_response := bus.process_call(battery_calculation_one): 1047 if battery_calculation_response.pgn.id != PGN["PropB_02", [32]].id: 1048 message = ( 1049 f"Expected PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name}) " 1050 f" but received PGN {battery_calculation_response.pgn.id}" 1051 ) 1052 logger.write_warning_to_html_report(message) 1053 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.WRONG_PGN.name 1054 return 1055 1056 for spn, elem in zip(PGN["PropB_02"].data_field, battery_calculation_response.data): 1057 if spn.name == "Battery State of Charge": 1058 low_range = 5 1059 high_range = 100 1060 comparison = f"{cmp(elem, '>=', low_range, '%')} and {cmp(elem, '<=', high_range, '%' )}" 1061 1062 if not low_range <= elem <= high_range: 1063 message = ( 1064 f"Skipping Test: {spn.name} is {elem}%, " 1065 f"which is not within expected range: {comparison}" 1066 ) 1067 logger.write_warning_to_html_report(message) 1068 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.SKIPPED_TEST.name 1069 1070 pytest.skip(message) 1071 else: 1072 message = f"{spn.name} is {elem}%" 1073 logger.write_result_to_html_report(message) 1074 break 1075 else: 1076 message = f"Did not receive response for PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name})" 1077 logger.write_warning_to_html_report(message) 1078 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.NO_RESPONSE.name 1079 pytest.skip(message) 1080 1081 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") and not hasattr( 1082 _GENERATED_PROFILE, "battery_regulation_two" 1083 ): 1084 default_b_one = self.get_battery_regulation_one() 1085 default_b_two = self.get_battery_regulation_two() 1086 else: 1087 default_b_one = _GENERATED_PROFILE.battery_regulation_one 1088 default_b_two = _GENERATED_PROFILE.battery_regulation_two 1089 1090 if isinstance(default_b_one, str) or isinstance(default_b_two, str): 1091 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.INVALID_RESPONSE.name 1092 1093 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 1094 pytest.skip("Missing valid Battery Regulation One and Two info") 1095 1096 logger.write_result_to_html_report("Discharging battery") 1097 try: 1098 cyber_charger = charger.Charger() 1099 except RuntimeError: 1100 message = "Could not communicate with power supply" 1101 logger.write_warning_to_html_report(message) 1102 pytest.fail(message) 1103 try: 1104 with cyber_charger(-0.1): 1105 logger.write_info_to_report("Battery Regulation One Values from Discharge") 1106 updated_b_one = self.get_battery_regulation_one() 1107 logger.write_info_to_report("Battery Regulation Two Values from Discharge") 1108 updated_b_two = self.get_battery_regulation_two() 1109 1110 if isinstance(updated_b_one, str): 1111 if updated_b_one == "No Current": 1112 _GENERATED_PROFILE.test_discharge_power_supply = "Current not detected" 1113 raise RuntimeError 1114 1115 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 1116 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 1117 pytest.skip("Missing valid Battery Regulation One and Two info") 1118 1119 _GENERATED_PROFILE.discharge_b_one = updated_b_one 1120 _GENERATED_PROFILE.discharge_b_two = updated_b_two 1121 1122 except RuntimeError: 1123 if cyber_charger.resource: 1124 if hasattr(cyber_charger.resource, "close"): 1125 cyber_charger.resource.close() 1126 message = "Could not communicate with power supply or current not detected" 1127 logger.write_warning_to_html_report(message) 1128 if not hasattr(_GENERATED_PROFILE, "test_charge_power_supply"): 1129 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 1130 pytest.fail(message) 1131 1132 except AttributeError: 1133 message = "Could not communicate with power supply" 1134 logger.write_warning_to_html_report(message) 1135 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 1136 pytest.fail(message) 1137 1138 def test_discharge_comparison(self): 1139 """ 1140 | Description | Compare Power Supply Discharge values | 1141 | :------------------- | :--------------------------------------------------------------- | 1142 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1143 | Instructions | 1. Check if discharge Battery Current is within 10% of initial value| 1144 | Pass / Fail Criteria | Pass if discharged value is within 10% | 1145 | Estimated Duration | 1 second | 1146 """ 1147 1148 if hasattr(_GENERATED_PROFILE, "test_discharge_power_supply") or not hasattr( 1149 _GENERATED_PROFILE, "discharge_b_one" 1150 ): 1151 message = "Nothing to compare. The profile test may have failed or been skipped" 1152 logger.write_result_to_html_report(message) 1153 pytest.skip(message) 1154 1155 if hasattr(_COMPARISON_PROFILE, "test_discharge_power_supply") or not hasattr( 1156 _COMPARISON_PROFILE, "discharge_b_one" 1157 ): 1158 message = "Nothing to compare. The profile test may have failed or been skipped" 1159 logger.write_result_to_html_report(message) 1160 pytest.skip(message) 1161 1162 if isinstance(_GENERATED_PROFILE.discharge_b_one, str) or isinstance(_COMPARISON_PROFILE.discharge_b_one, str): 1163 if not isinstance(_GENERATED_PROFILE.discharge_b_one, type(_COMPARISON_PROFILE.discharge_b_one)): 1164 message = "Unable to compare profiles, types of Battery Regulation Information 1 did not match" 1165 logger.write_failure_to_html_report(message) 1166 pytest.fail(message) 1167 1168 comparison = cmp(_GENERATED_PROFILE.discharge_b_one, "==", _COMPARISON_PROFILE.discharge_b_one) 1169 1170 if _GENERATED_PROFILE.discharge_b_one != _COMPARISON_PROFILE.discharge_b_one: 1171 message = f"Battery Regulation Information 1 matched profiles: {comparison}" 1172 logger.write_failure_to_html_report(message) 1173 pytest.fail(message) 1174 1175 message = f"Battery Regulation Information 1 matched: {comparison}" 1176 logger.write_result_to_html_report(message) 1177 return 1178 1179 original_current = _GENERATED_PROFILE.discharge_b_one.get("battery_current") 1180 comparison_current = _COMPARISON_PROFILE.discharge_b_one.get("battery_current") 1181 1182 diff = abs(original_current - comparison_current) 1183 average = abs(original_current + comparison_current) / 2 1184 percentage = round((diff / average), 4) 1185 percent_closeness = 0.1 1186 1187 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 1188 1189 if not math.isclose(original_current, comparison_current, rel_tol=0.1): 1190 logger.write_warning_to_html_report( 1191 f"Battery Regulation Information 1 current percent difference: {comparison}" 1192 ) 1193 logger.write_failure_to_html_report("Current was not within expected range of 10% during discharge") 1194 pytest.fail("Current was not within expected range of 10% during charge") 1195 1196 logger.write_result_to_html_report(f"Battery regulation Information 1 current percent difference: {comparison}") 1197 logger.write_result_to_html_report("Current was within expected range of 10% during discharge") 1198 1199 @staticmethod 1200 def get_battery_regulation_one() -> str | dict[str, int | Any]: 1201 """Obtain the Battery Regulation One information""" 1202 battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03]) 1203 logger.write_result_to_html_report("<span style='font-weight: bold'>Battery Regulation Information 1</span>") 1204 1205 with CANBus(BATTERY_CHANNEL) as bus: 1206 battery_regulation_one: str | dict[str, int | Any] 1207 if battery_frame := bus.process_call(battery_regulation_one_request): 1208 if battery_frame.pgn.id != PGN["PropB_03", [32]].id: 1209 message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}" 1210 logger.write_warning_to_html_report(message) 1211 battery_regulation_one = BadStates.WRONG_PGN.name 1212 else: 1213 battery_regulation_one = { 1214 "battery_voltage": battery_frame.data[0], 1215 "open_circuit_voltage": battery_frame.data[1], 1216 "battery_current": battery_frame.data[2], 1217 "maximum_charge_current": battery_frame.data[3], 1218 } 1219 1220 for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data): 1221 1222 if spn.name == "Reserved": 1223 continue 1224 1225 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1226 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1227 1228 if spn.name == "Battery Current": 1229 if -0.1 <= elem <= 0.1: 1230 message = ( 1231 "Current was not detected. The power supply leads may not be fully " 1232 "connected, or the fuse has blown" 1233 ) 1234 logger.write_warning_to_html_report(message) 1235 return "No Current" 1236 1237 else: 1238 message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)" 1239 logger.write_warning_to_html_report(message) 1240 battery_regulation_one = BadStates.NO_RESPONSE.name 1241 1242 return battery_regulation_one 1243 1244 @staticmethod 1245 def get_battery_regulation_two() -> str | dict[str, int | Any]: 1246 """Obtain the Battery Regulation Two information""" 1247 battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04]) 1248 logger.write_result_to_html_report("<span style='font-weight: bold'>Battery Regulation Information 2</span>") 1249 1250 with CANBus(BATTERY_CHANNEL) as bus: 1251 battery_regulation_two: str | dict[str, int | Any] 1252 if battery_frame_two := bus.process_call(battery_regulation_two_request): 1253 if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id: 1254 message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}" 1255 logger.write_warning_to_html_report(message) 1256 battery_regulation_two = BadStates.NO_RESPONSE.name 1257 else: 1258 battery_regulation_two = { 1259 "contractor_state": battery_frame_two.data[0], 1260 "charge_capability_state": battery_frame_two.data[1], 1261 "bus_voltage_request": battery_frame_two.data[3], 1262 "transportability_soc": battery_frame_two.data[4], 1263 } 1264 1265 for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data): 1266 1267 if spn.name == "Reserved": 1268 continue 1269 1270 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1271 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1272 1273 else: 1274 message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)" 1275 logger.write_warning_to_html_report(message) 1276 battery_regulation_two = BadStates.NO_RESPONSE.name 1277 1278 return battery_regulation_two 1279 1280 1281class TestConfigurationStateMessage: 1282 """Confirms Configuration State Message matches profile after factory reset""" 1283 1284 @pytest.mark.profile 1285 def test_read(self) -> None: 1286 """ 1287 | Description | Checks if Configuration State Message matches profile | 1288 | :------------------- | :--------------------------------------------------------------- | 1289 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1290 | Instructions | 1. Request Configuration State </br>\ 1291 2. Check if Configuration State matches default values </br>\ 1292 3. Log results | 1293 | Estimated Duration | 21 seconds | 1294 """ 1295 1296 configuration_state_request = CANFrame(pgn=PGN["Request"], data=[PGN["Configuration State Message 1"].id]) 1297 with CANBus(BATTERY_CHANNEL) as bus: 1298 if configuration_frame := bus.process_call(configuration_state_request): 1299 if configuration_frame.pgn.id != PGN["Configuration State Message 1", [32]].id: 1300 message = ( 1301 f"Received PGN {configuration_frame.pgn.id}, not PGN " 1302 f"{PGN['Configuration State Message 1', [32]].id} " 1303 ) 1304 logger.write_failure_to_html_report(message) 1305 _GENERATED_PROFILE.configuration_state_message = BadStates.WRONG_PGN.name 1306 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1307 return 1308 1309 data = configuration_frame.data 1310 pgn_data_field = PGN["Configuration State Message 1"].data_field 1311 1312 if len(data) != len(pgn_data_field): 1313 message = ( 1314 f"Expected {len(pgn_data_field)} data fields, " 1315 f"got {len(data)}. Data is missing from response" 1316 ) 1317 logger.write_warning_to_html_report(message) 1318 _GENERATED_PROFILE.configuration_state_message = BadStates.WRONG_PACKETS.name 1319 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1320 return 1321 1322 logger.write_result_to_html_report("<span style='font-weight: bold'>Configuration State Message</span>") 1323 1324 for spn, elem in zip(pgn_data_field, data): 1325 if spn.name == "Reserved": 1326 continue 1327 1328 if not 0 <= elem <= 3: 1329 message = ( 1330 f"{spn.name}: {cmp(elem, '>=', 0, f'({spn.data_type(elem)})')} and " 1331 f"{cmp(elem, '<=', 3, f'({spn.data_type(elem)})')}" 1332 ) 1333 logger.write_warning_to_html_report(message) 1334 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1335 continue 1336 1337 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1338 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1339 1340 if spn.name in ("Battery Battle-override State", "Standby State", "Configure VPMS Function State"): 1341 message = ( 1342 f"{spn.name}: {cmp(elem, '==', 0, f'({spn.data_type(elem)})', f'({spn.data_type(0)})')}" 1343 ) 1344 if elem != 0: 1345 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1346 logger.write_warning_to_html_report(message) 1347 1348 if spn.name in ("Automated Heater Function State", "Contactor(s) Control State"): 1349 message = ( 1350 f"{spn.name}: {cmp(elem, '==', 1, f'({spn.data_type(elem)})', f'({spn.data_type(1)})')}" 1351 ) 1352 if elem != 1: 1353 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1354 logger.write_warning_to_html_report(message) 1355 1356 configuration_state_message = { 1357 "dormant_1_state": data[0], 1358 "dormant_2_state": data[1], 1359 "master_power_switch": data[2], 1360 "configuration_pin_1_state": data[3], 1361 "configuration_pin_2_state": data[4], 1362 "configuration_pin_3_state": data[5], 1363 "configuration_pin_4_state": data[6], 1364 "configuration_pin_5_state": data[7], 1365 "configuration_pin_6_state": data[8], 1366 "virtual_master_power_switch_state": data[9], 1367 "battery_battle_override_state": data[11], 1368 "maintenance_state": data[12], 1369 "automated_heater_function_state": data[13], 1370 "battery_heater_state": data[14], 1371 "contractor_control_state": data[15], 1372 "standby_state": data[16], 1373 "baud_rate_overwrite_state": data[18], 1374 "position_identity_overwrite_state": data[19], 1375 "configure_vpms_function_state": data[20], 1376 "pulse_power_control_state": data[21], 1377 } 1378 1379 _GENERATED_PROFILE.configuration_state_message = configuration_state_message 1380 1381 if not hasattr(_GENERATED_PROFILE, "configuration_state_specification"): 1382 _GENERATED_PROFILE.configuration_state_specification = "valid" 1383 1384 else: 1385 message = ( 1386 f"Did not receive a response for PGN {PGN['Configuration State Message 1', [32]].id} " 1387 f"(Configuration State Message 1)" 1388 ) 1389 _GENERATED_PROFILE.configuration_state_message = BadStates.NO_RESPONSE.name 1390 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1391 logger.write_warning_to_html_report(message) 1392 1393 def test_comparison(self) -> None: 1394 """ 1395 | Description | Compare Configuration State Message 1 values | 1396 | :------------------- | :--------------------------------------------------------------- | 1397 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1398 | Instructions | 1. Check if Configuration State Message 1 values match | 1399 | Pass / Fail Criteria | Pass if Configuration State Message 1 values match | 1400 | Estimated Duration | 1 second | 1401 """ 1402 failed_test = False 1403 if not hasattr(_GENERATED_PROFILE, "configuration_state_message") or not hasattr( 1404 _COMPARISON_PROFILE, "configuration_state_message" 1405 ): 1406 message = "Nothing to compare. The profile test may have failed or been skipped" 1407 logger.write_result_to_html_report(message) 1408 pytest.skip(message) 1409 1410 failed_categories = [] 1411 1412 logger.write_result_to_html_report( 1413 "<span style='font-weight: bold'>Configuration State Message Comparisons</span>" 1414 ) 1415 1416 if isinstance(_GENERATED_PROFILE.configuration_state_message, str) or isinstance( 1417 _COMPARISON_PROFILE.configuration_state_message, str 1418 ): 1419 if not isinstance( 1420 _GENERATED_PROFILE.configuration_state_message, type(_COMPARISON_PROFILE.configuration_state_message) 1421 ): 1422 message = "Unable to compare profiles, Configuration State Message types did not match" 1423 logger.write_failure_to_html_report(message) 1424 pytest.fail(message) 1425 1426 comparison = cmp( 1427 _GENERATED_PROFILE.configuration_state_message, "==", _COMPARISON_PROFILE.configuration_state_message 1428 ) 1429 1430 if _GENERATED_PROFILE.configuration_state_message != _COMPARISON_PROFILE.configuration_state_message: 1431 message = f"Configuration State Message did not match: {comparison}" 1432 logger.write_failure_to_html_report(message) 1433 pytest.fail(message) 1434 1435 message = f"Configuration State Message matched: {comparison}" 1436 logger.write_result_to_html_report(message) 1437 else: 1438 1439 for key, value in _GENERATED_PROFILE.configuration_state_message.items(): 1440 key_text = key.replace("_", " ").title() 1441 comparison = cmp(value, "==", _COMPARISON_PROFILE.configuration_state_message[key]) 1442 message = f"{key_text}: {comparison}" 1443 1444 if value != _COMPARISON_PROFILE.configuration_state_message[key]: 1445 logger.write_warning_to_html_report(message) 1446 failed_categories.append(key_text) 1447 else: 1448 logger.write_result_to_html_report(message) 1449 1450 fail_length = len(failed_categories) 1451 1452 if fail_length > 0: 1453 categories = "data values" if fail_length > 1 else "data value" 1454 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 1455 logger.write_failure_to_html_report(message) 1456 failed_test = True 1457 1458 if not hasattr(_GENERATED_PROFILE, "configuration_state_specification") or not hasattr( 1459 _COMPARISON_PROFILE, "configuration_state_specification" 1460 ): 1461 logger.write_warning_to_html_report("Missing specification information") 1462 else: 1463 comparison = cmp( 1464 _GENERATED_PROFILE.configuration_state_specification, 1465 "==", 1466 _COMPARISON_PROFILE.configuration_state_specification, 1467 ) 1468 if ( 1469 _GENERATED_PROFILE.configuration_state_specification 1470 != _COMPARISON_PROFILE.configuration_state_specification 1471 ): 1472 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1473 failed_test = True 1474 else: 1475 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1476 1477 if failed_test: 1478 message = "Comparisons did not match in profiles" 1479 logger.write_failure_to_html_report(message) 1480 pytest.fail(message) 1481 1482 logger.write_result_to_html_report("Configuration State Message 1 values successfully matched") 1483 1484 1485def na_maintenance_mode(bus: CANBus) -> bool: 1486 """This function will place battery in Maintenance Mode with Reset Value of "3".""" 1487 1488 maintenance_mode = CANFrame( # Memory access only works in maintenance mode 1489 destination_address=_GENERATED_PROFILE.address, 1490 pgn=PGN["PropA"], 1491 data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1], 1492 ) 1493 1494 bus.send_message(maintenance_mode.message()) # Enter maintenance mode 1495 if response := bus.read_input(): 1496 ack_frame = CANFrame.decode(response.arbitration_id, response.data) 1497 if ack_frame.pgn.id != 0xE800 and ack_frame.data[0] != 0: 1498 logger.write_warning_to_html_report("Unable to enter maintenance mode") 1499 return False 1500 return True 1501 1502 message = "Received no maintenance mode response" 1503 logger.write_warning_to_html_report(message) 1504 return False 1505 1506 1507class TestNameManagementMessage: 1508 """This will test the mandatory NAME Management commands""" 1509 1510 @pytest.mark.profile 1511 def test_name_management_command(self) -> None: 1512 """ 1513 | Description | Test Name Management Command | 1514 | :------------------- | :--------------------------------------------------------------- | 1515 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1516 | Instructions | 1. Get NAME information from Address Claimed </br>\ 1517 2. Change ECU value </br>\ 1518 3. Test Name Management Command </br>\ 1519 4. Check Values updated </br>\ 1520 5. Log results | 1521 | Estimated Duration | 21 seconds | 1522 """ 1523 1524 old_ecu_value = 0 1525 new_ecu_value = 0 1526 checksum_value = 0 1527 adopt_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1528 address_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 1529 1530 with CANBus(BATTERY_CHANNEL) as bus: 1531 entered_maintenance_mode = na_maintenance_mode(bus) 1532 if not entered_maintenance_mode: 1533 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1534 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1535 1536 # Name Management Test 1537 if address_claimed := bus.process_call(address_request): 1538 if address_claimed.pgn.id != PGN["Address Claimed", [32]].id: 1539 Errors.unexpected_packet("Address Claimed", address_claimed) 1540 message = f"Unexpected packet PGN {address_claimed.pgn.id} was received" 1541 logger.write_warning_to_html_report(message) 1542 _GENERATED_PROFILE.name_management_ecu_value = BadStates.WRONG_PACKETS.name 1543 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1544 return 1545 1546 checksum_value = sum(address_claimed.packed_data) & 0xFF 1547 old_ecu_value = address_claimed.data[2] 1548 if old_ecu_value > 0: 1549 new_ecu_value = 0 1550 else: 1551 new_ecu_value = 1 1552 else: 1553 message = f"No response received for PGN {[PGN['Address Claimed', [32]].id]}" 1554 logger.write_warning_to_html_report(message) 1555 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1556 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1557 return 1558 1559 if not checksum_value: 1560 message = "Could not condense Name into bits for NAME Management" 1561 logger.write_warning_to_html_report(message) 1562 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1563 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1564 return 1565 1566 request_data: list[float] = [ 1567 checksum_value, 1568 1, 1569 0, 1570 1, 1571 1, 1572 1, 1573 1, 1574 1, 1575 1, 1576 0, 1577 1, 1578 1, 1579 new_ecu_value, 1580 1, 1581 1, 1582 1, 1583 1, 1584 1, 1585 1, 1586 1, 1587 ] 1588 name_management_set_name = CANFrame( 1589 destination_address=_GENERATED_PROFILE.address, pgn=PGN["NAME Management Message"], data=request_data 1590 ) 1591 1592 if pending_response := bus.process_call(name_management_set_name): 1593 if pending_response.pgn.id != PGN["NAME Management Message", [32]].id: 1594 if pending_response.pgn.id == PGN["Acknowledgement", [32]].id: 1595 message = ( 1596 f"Battery may not support NAME Management, received PGN " 1597 f"{pending_response.pgn.id} ({spn_types.acknowledgement(pending_response.data[0])})" 1598 ) 1599 logger.write_warning_to_html_report(message) 1600 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NACK.name 1601 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1602 1603 else: 1604 Errors.unexpected_packet("NAME Management Message", pending_response) 1605 message = f"Unexpected PGN {pending_response.pgn.id} received" 1606 logger.write_warning_to_html_report(message) 1607 _GENERATED_PROFILE.name_management_ecu_value = BadStates.WRONG_PGN.name 1608 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1609 1610 return 1611 1612 if pending_response.data[0] == 3: 1613 message = ( 1614 f"Message was unsuccessful, received error: " 1615 f"{spn_types.name_error_code(pending_response.data[0])}" 1616 ) 1617 logger.write_warning_to_html_report(message) 1618 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1619 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1620 return 1621 1622 if pending_response.data[12] != new_ecu_value: 1623 message = f"ECU Value was not changed from {old_ecu_value} to {new_ecu_value}" 1624 logger.write_warning_to_html_report(message) 1625 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1626 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1627 return 1628 else: 1629 message = ( 1630 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1631 ) 1632 logger.write_warning_to_html_report(message) 1633 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1634 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1635 return 1636 1637 adopt_name_request = CANFrame( 1638 destination_address=_GENERATED_PROFILE.address, 1639 pgn=PGN["NAME Management Message"], 1640 data=adopt_name_request_data, 1641 ) 1642 1643 bus.send_message(adopt_name_request.message()) 1644 1645 current_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1646 current_name_request = CANFrame( 1647 destination_address=_GENERATED_PROFILE.address, 1648 pgn=PGN["NAME Management Message"], 1649 data=current_name_request_data, 1650 ) 1651 1652 if name_management_response := bus.process_call(current_name_request): 1653 if name_management_response.data[12] != new_ecu_value: 1654 message = ( 1655 f"Name's ECU Instance was not updated after Name Management Changes, " 1656 f"{cmp(name_management_response.data[12], '==', new_ecu_value)}" 1657 ) 1658 logger.write_warning_to_html_report(message) 1659 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1660 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1661 return 1662 else: 1663 message = ( 1664 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1665 ) 1666 logger.write_warning_to_html_report(message) 1667 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1668 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1669 return 1670 1671 if new_address_claimed := bus.process_call(address_request): 1672 if new_address_claimed.data[2] != new_ecu_value + 1: 1673 message = ( 1674 f"Address Claimed was not updated after Name Management Changes. " 1675 f"{cmp(new_address_claimed.data[2], '==', new_ecu_value + 1)}" 1676 ) 1677 logger.write_warning_to_html_report(message) 1678 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1679 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1680 return 1681 1682 _GENERATED_PROFILE.name_management_ecu_value = address_claimed.data[2] 1683 logger.write_result_to_html_report( 1684 f"Name Management Command was successful. " 1685 f"ECU Instance was changed from {old_ecu_value} to {new_ecu_value}" 1686 ) 1687 _GENERATED_PROFILE.name_management_ecu_specification = "valid" 1688 1689 @pytest.mark.profile 1690 def test_wrong_name_management_data(self): 1691 """ 1692 | Description | Test Incorrect Data for NAME Management Command | 1693 | :------------------- | :--------------------------------------------------------------- | 1694 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1695 | Instructions | 1. Provide wrong Checksum value for Name Command </br>\ 1696 2. Check receive Checksum error code as response </br>\ 1697 3. Log results | 1698 | Estimated Duration | 1 second | 1699 """ 1700 wrong_checksum = 0 1701 new_ecu_value = 0 1702 1703 current_name_request_data = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1704 current_name_request = CANFrame( 1705 destination_address=_GENERATED_PROFILE.address, 1706 pgn=PGN["NAME Management Message"], 1707 data=current_name_request_data, 1708 ) 1709 1710 with CANBus(BATTERY_CHANNEL) as bus: 1711 # Test Checksum Error -- Should return Error Code: 3 1712 in_maintenance_mode = na_maintenance_mode(bus) 1713 1714 if not in_maintenance_mode: 1715 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1716 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1717 1718 if current_name := bus.process_call(current_name_request): 1719 if current_name.pgn.id != PGN["NAME Management Message", [32]].id: 1720 if current_name.pgn.id == PGN["Acknowledgement", [32]].id: 1721 message = ( 1722 f"Battery may not support NAME Management, received PGN " 1723 f"{current_name.pgn.id} ({spn_types.acknowledgement(current_name.data[0])})" 1724 ) 1725 logger.write_warning_to_html_report(message) 1726 _GENERATED_PROFILE.wrong_name_data = BadStates.NACK.name 1727 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1728 return 1729 Errors.unexpected_packet("NAME Management Message", current_name) 1730 message = f"Unexpected packet was received: PGN {current_name.pgn.id}" 1731 logger.write_warning_to_html_report(message) 1732 _GENERATED_PROFILE.wrong_name_data = BadStates.WRONG_PACKETS.name 1733 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1734 return 1735 1736 wrong_checksum = sum(current_name.packed_data) & 0xFF 1737 old_ecu_value = current_name.data[12] 1738 if old_ecu_value > 0: 1739 new_ecu_value = 0 1740 else: 1741 new_ecu_value = 1 1742 else: 1743 message = ( 1744 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1745 ) 1746 logger.write_warning_to_html_report(message) 1747 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1748 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1749 return 1750 1751 request_data = [wrong_checksum, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, new_ecu_value, 1, 1, 1, 1, 1, 1, 1] 1752 name_management_set_name = CANFrame( 1753 destination_address=_GENERATED_PROFILE.address, pgn=PGN["NAME Management Message"], data=request_data 1754 ) 1755 1756 if pending_response := bus.process_call(name_management_set_name): 1757 if pending_response.pgn.id != PGN["NAME Management Message", [32]].id: 1758 if pending_response.pgn.id == PGN["Acknowledgement", [32]].id: 1759 message = ( 1760 f"Battery may not support NAME Management, received PGN {pending_response.pgn.id} " 1761 f"({spn_types.acknowledgement(pending_response.data[0])})" 1762 ) 1763 logger.write_warning_to_html_report(message) 1764 _GENERATED_PROFILE.wrong_name_data = BadStates.NACK.name 1765 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1766 return 1767 1768 Errors.unexpected_packet("NAME Management Message", pending_response) 1769 message = f"Unexpected packet was received: {pending_response.pgn.id}" 1770 logger.write_warning_to_html_report(message) 1771 _GENERATED_PROFILE.wrong_name_data = BadStates.WRONG_PGN.name 1772 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1773 return 1774 1775 if pending_response.data[0] != 3: 1776 message = cmp( 1777 pending_response.data[0], 1778 "==", 1779 3, 1780 f"({spn_types.name_error_code(pending_response.data[0])})", 1781 f"({spn_types.name_error_code(3)})", 1782 ) 1783 logger.write_warning_to_html_report( 1784 f"PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message) " 1785 f"updated NAME Management with incorrect checksum value" 1786 ) 1787 logger.write_warning_to_html_report(message) 1788 _GENERATED_PROFILE.wrong_name_data = "Incorrect checksum value" 1789 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1790 return 1791 else: 1792 message = ( 1793 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1794 ) 1795 logger.write_warning_to_html_report(message) 1796 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1797 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1798 return 1799 1800 logger.write_result_to_html_report( 1801 "NAME Management successfully did not process request with incorrect checksum value" 1802 ) 1803 _GENERATED_PROFILE.wrong_name_data = "Passed" 1804 _GENERATED_PROFILE.wrong_name_specification = "valid" 1805 1806 def test_name_comparison(self) -> None: 1807 """ 1808 | Description | Compare if Name Management ECU values were changed | 1809 | :------------------- | :--------------------------------------------------------------- | 1810 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1811 | Instructions | 1. Check if Name Management ECU values matched in profiles | 1812 | Pass / Fail Criteria | Pass if Name Management ECU values match | 1813 | Estimated Duration | 1 second | 1814 """ 1815 failed_test = False 1816 if not hasattr(_GENERATED_PROFILE, "name_management_ecu_value") or not hasattr( 1817 _COMPARISON_PROFILE, "name_management_ecu_value" 1818 ): 1819 message = "Nothing to compare. The profile test may have failed or been skipped" 1820 logger.write_result_to_html_report(message) 1821 pytest.skip(message) 1822 1823 comparison = cmp( 1824 _GENERATED_PROFILE.name_management_ecu_value, "==", _COMPARISON_PROFILE.name_management_ecu_value 1825 ) 1826 1827 if _GENERATED_PROFILE.name_management_ecu_value != _COMPARISON_PROFILE.name_management_ecu_value: 1828 comparison = cmp( 1829 _GENERATED_PROFILE.name_management_ecu_value, "==", _COMPARISON_PROFILE.name_management_ecu_value 1830 ) 1831 message = f"Name Management ECU values did not match: {comparison}" 1832 logger.write_failure_to_html_report(message) 1833 failed_test = True 1834 1835 logger.write_result_to_html_report(f"Name Management ECU values matched: {comparison}") 1836 1837 if not hasattr(_GENERATED_PROFILE, "name_management_ecu_specification") or not hasattr( 1838 _COMPARISON_PROFILE, "name_management_ecu_specification" 1839 ): 1840 logger.write_warning_to_html_report("Missing specification information") 1841 else: 1842 comparison = cmp( 1843 _GENERATED_PROFILE.name_management_ecu_specification, 1844 "==", 1845 _COMPARISON_PROFILE.name_management_ecu_specification, 1846 ) 1847 if ( 1848 _GENERATED_PROFILE.name_management_ecu_specification 1849 != _COMPARISON_PROFILE.name_management_ecu_specification 1850 ): 1851 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1852 failed_test = True 1853 else: 1854 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1855 1856 if failed_test: 1857 message = "Comparisons did not match in profiles" 1858 logger.write_failure_to_html_report(message) 1859 pytest.fail(message) 1860 1861 def test_wrong_name_comparison(self) -> None: 1862 """ 1863 | Description | Compare if Name Management values were changed with wrong data | 1864 | :------------------- | :--------------------------------------------------------------- | 1865 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1866 | Instructions | 1. Check if Name Management ECU values matched in profiles | 1867 | Pass / Fail Criteria | Pass if Name Management ECU values match | 1868 | Estimated Duration | 1 second | 1869 """ 1870 failed_test = False 1871 if not hasattr(_GENERATED_PROFILE, "wrong_name_data") or not hasattr(_COMPARISON_PROFILE, "wrong_name_data"): 1872 message = "Nothing to compare. The profile test may have failed or been skipped" 1873 logger.write_result_to_html_report(message) 1874 pytest.skip(message) 1875 1876 if _GENERATED_PROFILE.wrong_name_data != _COMPARISON_PROFILE.wrong_name_data: 1877 comparison = f"{_GENERATED_PROFILE.wrong_name_data} ≠ { _COMPARISON_PROFILE.wrong_name_data}" 1878 message = f"Name Management values did not match from Wrong Name Management test: {comparison}" 1879 logger.write_failure_to_html_report(message) 1880 failed_test = True 1881 else: 1882 logger.write_result_to_html_report( 1883 f"Name Management values had same results from Wrong Name Management test: " 1884 f"{_GENERATED_PROFILE.wrong_name_data}" 1885 ) 1886 1887 if not hasattr(_GENERATED_PROFILE, "wrong_name_specification") or not hasattr( 1888 _COMPARISON_PROFILE, "wrong_name_specification" 1889 ): 1890 logger.write_warning_to_html_report("Missing specification information") 1891 else: 1892 comparison = cmp( 1893 _GENERATED_PROFILE.wrong_name_specification, 1894 "==", 1895 _COMPARISON_PROFILE.wrong_name_specification, 1896 ) 1897 if _GENERATED_PROFILE.wrong_name_specification != _COMPARISON_PROFILE.wrong_name_specification: 1898 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1899 failed_test = True 1900 else: 1901 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1902 1903 if failed_test: 1904 message = "Comparisons did not match in profiles" 1905 logger.write_failure_to_html_report(message) 1906 pytest.fail(message) 1907 1908 1909class TestActiveDiagnosticTroubleCodes: 1910 """Test that Active Diagnostic Trouble Codes are J1939 Compliant""" 1911 1912 @pytest.mark.profile 1913 def test_profile(self) -> None: 1914 """ 1915 | Description | Test Active Diagnostic Trouble Codes (DM1) | 1916 | :------------------- | :--------------------------------------------------------------- | 1917 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1918 | Instructions | 1. Request Active Diagnostic Trouble Codes </br>\ 1919 2. Test Command Addresses command </br>\ 1920 3. Confirm Address was updated </br>\ 1921 4. Log results | 1922 | Estimated Duration | 21 seconds | 1923 """ 1924 dm1_request = CANFrame(pgn=PGN["Request"], data=[PGN["Active Diagnostic Trouble Codes", [32]].id]) 1925 with CANBus(BATTERY_CHANNEL) as bus: 1926 if dm1_frame := bus.process_call(dm1_request): 1927 if dm1_frame.pgn.id != PGN["Active Diagnostic Trouble Codes", [32]].id: 1928 Errors.unexpected_packet("Active Diagnostic Trouble Codes", dm1_frame) 1929 message = f"Unexpected data packet PGN {dm1_frame.pgn.id} was received" 1930 logger.write_warning_to_html_report(message) 1931 _GENERATED_PROFILE.active_diagnostic_trouble_codes = BadStates.WRONG_PGN.name 1932 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 1933 return 1934 1935 logger.write_result_to_html_report( 1936 "<span style='font-weight: bold'>Active Diagnostic Trouble Codes</span>" 1937 ) 1938 1939 dm1_data = dm1_frame.data 1940 pgn_data_field = PGN["Active Diagnostic Trouble Codes"].data_field 1941 1942 for spn, elem in zip(pgn_data_field, dm1_data): 1943 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1944 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1945 high_value = 1 1946 1947 if spn.name in ( 1948 "Protect Lamp", 1949 "Amber Warning Lamp", 1950 "Red Stop Lamp", 1951 "Malfunction Indicator Lamp", 1952 "DTC1.SPN_Conversion_Method", 1953 ): 1954 high_value = 1 1955 1956 if spn.name in ( 1957 "Flash Protect Lamp", 1958 "Flash Amber Warning Lamp", 1959 "Flash Red Stop Lamp", 1960 "Flash Malfunction Indicator Lamp", 1961 ): 1962 high_value = 3 1963 1964 if spn.name == "DTC1.Suspect_Parameter_Number": 1965 high_value = 524287 1966 1967 if spn.name == "DTC1.Failure_Mode_Identifier": 1968 high_value = 31 1969 1970 if spn.name == "DTC1.Occurrence_Count": 1971 high_value = 126 1972 1973 if not 0 <= elem <= high_value: 1974 logger.write_warning_to_html_report( 1975 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 1976 f"{cmp(elem, '<=', high_value, description)}" 1977 ) 1978 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 1979 1980 _GENERATED_PROFILE.active_diagnostic_trouble_codes = { 1981 "protect_lamp": dm1_data[0], 1982 "amber_warning_lamp": dm1_data[1], 1983 "red_stop_lamp": dm1_data[2], 1984 "malfunction_indicator_lamp": dm1_data[3], 1985 "flash_protect_lamp": dm1_data[4], 1986 "flash_amber_warning_lamp": dm1_data[5], 1987 "flash_red_stop_lamp": dm1_data[6], 1988 "flash_malfunction_indicator_lamp": dm1_data[7], 1989 "dtc1_suspect_parameter_number": dm1_data[8], 1990 "dtc1_failure_mode_identifier": dm1_data[9], 1991 "dtc1_occurrence_count": dm1_data[10], 1992 "dtc1_spn_conversion_method": dm1_data[11], 1993 } 1994 1995 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_specification"): 1996 _GENERATED_PROFILE.active_diagnostic_specification = "valid" 1997 1998 else: 1999 message = ( 2000 f"No response was received for mandatory command: " 2001 f"PGN {PGN['Active Diagnostic Trouble Codes', [32]].id} (Active Diagnostic Trouble Codes)" 2002 ) 2003 logger.write_warning_to_html_report(message) 2004 _GENERATED_PROFILE.active_diagnostic_trouble_codes = BadStates.NO_RESPONSE.name 2005 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 2006 2007 def test_comparison(self) -> None: 2008 """ 2009 | Description | Compare if Active Diagnostic Trouble Codes matched | 2010 | :------------------- | :--------------------------------------------------------------- | 2011 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 2012 | Instructions | 1. Check if Active Diagnostic Trouble Codes matched in profiles | 2013 | Pass / Fail Criteria | Pass if Active Diagnostic Trouble Codes match | 2014 | Estimated Duration | 1 second | 2015 """ 2016 failed_test = False 2017 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_trouble_codes") or not hasattr( 2018 _COMPARISON_PROFILE, "active_diagnostic_trouble_codes" 2019 ): 2020 message = "Nothing to compare. The profile test may have failed or been skipped" 2021 logger.write_result_to_html_report(message) 2022 pytest.skip(message) 2023 2024 if isinstance(_GENERATED_PROFILE.active_diagnostic_trouble_codes, str) or isinstance( 2025 _COMPARISON_PROFILE.active_diagnostic_trouble_codes, str 2026 ): 2027 if not isinstance( 2028 _GENERATED_PROFILE.active_diagnostic_trouble_codes, 2029 type(_COMPARISON_PROFILE.active_diagnostic_trouble_codes), 2030 ): 2031 message = "Unable to compare profiles, Active Diagnostic Trouble Codes types did not match" 2032 logger.write_failure_to_html_report(message) 2033 pytest.fail(message) 2034 2035 comparison = cmp( 2036 _GENERATED_PROFILE.active_diagnostic_trouble_codes, 2037 "==", 2038 _COMPARISON_PROFILE.active_diagnostic_trouble_codes, 2039 ) 2040 2041 if ( 2042 _GENERATED_PROFILE.active_diagnostic_trouble_codes 2043 != _COMPARISON_PROFILE.active_diagnostic_trouble_codes 2044 ): 2045 message = f"Active Diagnostic Trouble Codes did not match: {comparison}" 2046 logger.write_failure_to_html_report(message) 2047 pytest.fail(message) 2048 2049 message = f"Active Diagnostic Trouble Codes matched: {comparison}" 2050 logger.write_result_to_html_report(message) 2051 else: 2052 failed_categories = [] 2053 for key, value in _GENERATED_PROFILE.active_diagnostic_trouble_codes.items(): 2054 key_text = key.replace("_", " ").title() 2055 2056 if key == "dtc1_suspect_parameter_number": 2057 key_text = "DTC1.Suspect_Parameter_Number" 2058 2059 if key == "dtc1_failure_mode_identifier": 2060 key_text = "DTC1.Failure_Mode_Identifier" 2061 2062 if key == "dtc1_occurrence_count": 2063 key_text = "DTC1.Occurrence_Count" 2064 2065 if key == "dtc1_spn_conversion_method": 2066 key_text = "DTC1.SPN_Conversion_Method" 2067 2068 comparison = cmp(value, "==", _COMPARISON_PROFILE.active_diagnostic_trouble_codes[key]) 2069 message = f"{key_text}: {comparison}" 2070 2071 if value != _COMPARISON_PROFILE.active_diagnostic_trouble_codes[key]: 2072 logger.write_warning_to_html_report(message) 2073 failed_categories.append(key_text) 2074 else: 2075 logger.write_result_to_html_report(message) 2076 2077 fail_length = len(failed_categories) 2078 2079 if fail_length > 0: 2080 categories = "data values" if fail_length > 1 else "data value" 2081 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2082 logger.write_failure_to_html_report(message) 2083 failed_test = True 2084 else: 2085 logger.write_result_to_html_report("Active Diagnostic Trouble Code values between profiles matched") 2086 2087 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_specification") or not hasattr( 2088 _COMPARISON_PROFILE, "active_diagnostic_specification" 2089 ): 2090 logger.write_warning_to_html_report("Missing specification information") 2091 else: 2092 comparison = cmp( 2093 _GENERATED_PROFILE.active_diagnostic_specification, 2094 "==", 2095 _COMPARISON_PROFILE.active_diagnostic_specification, 2096 ) 2097 if ( 2098 _GENERATED_PROFILE.active_diagnostic_specification 2099 != _COMPARISON_PROFILE.active_diagnostic_specification 2100 ): 2101 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2102 failed_test = True 2103 else: 2104 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2105 2106 if failed_test: 2107 message = "Comparisons did not match in profiles" 2108 logger.write_failure_to_html_report(message) 2109 pytest.fail(message) 2110 2111 2112# TODO(DF): Fix Stop Start Broadcast Test when battery is able to broadcast, test currently blocked 2113# class TestStopStartBroadcast: 2114# """Test that Stop/Start Broadcast is J1939 Compliant""" 2115# 2116# @pytest.mark.profile 2117# def test_profile(self) -> None: 2118# """ 2119# | Description | Test that Stop/Start Broadcast (DM13) | 2120# | :------------------- | :--------------------------------------------------------------- | 2121# | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 2122# | Instructions | 1. Create Start Broadcast request </br>\ 2123# 2. Check broadcast began </br>\ 2124# 3. Create Stop Broadcast request </br>\ 2125# 4. Check broadcast stopped </br>\ 2126# 5. Create Suspend Broadcast request </br>\ 2127# 6. Check broadcast suspended </br>\ 2128# 7. Log results | 2129# | Estimated Duration | 1 second | 2130# """ 2131# 2132# with CANBus(BATTERY_CHANNEL) as bus: 2133# na_maintenance_mode(bus) 2134# TODO(DF): Determine order of data (either SAFT or order in j1939da.py) 2135# broadcast_message_data = { 2136# "current_data_link": 1, 2137# "sae_j1587": 3, 2138# "sae_j1922": 3, 2139# "sae_j1939_network_1": 1, 2140# "sae_j1939_network_2": 3, 2141# "iso_9141": 3, 2142# "sae_j1850": 3, 2143# "other_manufacturer_specified_port": 3, 2144# "sae_j1939_network #3": 3, 2145# "proprietary_network_1": 3, 2146# "proprietary_network_2": 3, 2147# "sae_j1939_network_4": 3, 2148# "hold_signal": 15, 2149# "suspend_signal": 255, 2150# "suspend_duration": 0, 2151# "sae_j1939_network_5": 3, 2152# "sae_j1939_network_6": 3, 2153# "sae_j1939_network_7": 3, 2154# "sae_j1939_network_8": 3, 2155# "reserved": 2, 2156# "sae_j1939_network_9": 3, 2157# "sae_j1939_network_10": 3, 2158# "sae_j1939_network_11": 3, 2159# } 2160# # Order from SAFT Documentation 2161# # broadcast_message_data = { 2162# # "sae_j1939_network_1": 1, 2163# # "sae_j1922": 3, 2164# # "sae_j1587": 3, 2165# # "current_data_link": 1, 2166# # "other_manufacturer_specified_port": 3, 2167# # "sae_j1850": 3, 2168# # "iso_9141": 3, 2169# # "sae_j1939_network_2": 3, 2170# # "sae_j1939_network_4": 3, 2171# # "proprietary_network_2": 3, 2172# # "proprietary_network_1": 3, 2173# # "sae_j1939_network #3": 3, 2174# # "suspend_signal": 255, 2175# # "hold_signal": 15, 2176# # "suspend_duration": 0, 2177# # "sae_j1939_network_8": 3, 2178# # "sae_j1939_network_7": 3, 2179# # "sae_j1939_network_6": 3, 2180# # "sae_j1939_network_5": 3, 2181# # "sae_j1939_network_11": 3, 2182# # "sae_j1939_network_10": 3, 2183# # "sae_j1939_network_9": 3, 2184# # "reserved": 2, 2185# # } 2186# 2187# dm1_start_request_data = list(broadcast_message_data.values()) 2188# 2189# # Start Broadcast 2190# dm1_request = CANFrame(pgn=PGN["Stop Start Broadcast"], data=dm1_start_request_data) 2191# bus.send_message(dm1_request.message()) 2192# 2193# # Check broadcast 2194# if mystery_response := bus.read_input(): 2195# logger.write_info_to_report("Broadcast was successfully started") 2196# logger.write_info_to_report(f"First broadcast received: {mystery_response}") 2197# else: 2198# message = "Broadcast did not start running Stop Start Broadcast command" 2199# logger.write_warning_to_report(message) 2200# pytest.fail(message) 2201# 2202# # Stop Broadcast 2203# broadcast_message_data["sae_j1939_network_1"] = 3 2204# broadcast_message_data["current_data_link"] = 3 2205# broadcast_message_data["hold_signal"] = 1 2206# dm1_stop_request_data = list(broadcast_message_data.values()) 2207# 2208# dm1_request = CANFrame(pgn=PGN["Stop Start Broadcast"], data=dm1_stop_request_data) 2209# bus.send_message(dm1_request.message()) 2210# 2211# if mystery_response := bus.read_input(): 2212# message = "Broadcast did not stop running after Stop Start Broadcast command" 2213# logger.write_warning_to_report(message) 2214# logger.write_info_to_report(f"Broadcast received: {mystery_response}") 2215# 2216# else: 2217# message = "Broadcast was succesfully stopped after Stop Start Broadcast command" 2218# logger.write_info_to_report(message) 2219# 2220# broadcast_message_data["suspend_signal"] = 0 2221# dm1_suspend_request_data = list(broadcast_message_data.values()) 2222# 2223# dm1_request = CANFrame(pgn=PGN["Stop Start Broadcast"], data=dm1_suspend_request_data) 2224# bus.send_message(dm1_request.message()) 2225# 2226# if mystery_response := bus.read_input(): 2227# message = "Broadcast was not suspended after Stop Start Broadcast command" 2228# logger.write_warning_to_report(message) 2229# logger.write_warning_to_report(f"Broadcast received: {mystery_response}") 2230# else: 2231# message = "Broadcast was suspended after Stop Start Broadcast command" 2232# logger.write_info_to_report(message) 2233# 2234# logger.write_result_to_html_report("Stop Start Broadcast command was successful") 2235# 2236 2237 2238class TestVehicleElectricalPower: 2239 """Test Vehicle Electrical Power command is J1939 Compliant""" 2240 2241 @pytest.mark.profile 2242 def test_profile(self) -> None: 2243 """ 2244 | Description | Test Vehicle Electrical Power #5 (VEP5) command | 2245 | :------------------- | :--------------------------------------------------------------- | 2246 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 2247 | Instructions | 1. Send Vehicle Electrical Power #5 command </br>\ 2248 2. Check values in data </br>\ 2249 3. Log response | 2250 | Estimated Duration | 21 seconds | 2251 """ 2252 2253 with CANBus(BATTERY_CHANNEL) as bus: 2254 vehicle_electrical_power_request = CANFrame( 2255 destination_address=_GENERATED_PROFILE.address, 2256 pgn=PGN["Request"], 2257 data=[PGN["Vehicle Electrical Power #5"].id], 2258 ) 2259 if vehicle_electrical_power_response := bus.process_call(vehicle_electrical_power_request): 2260 if vehicle_electrical_power_response.pgn.id != PGN["Vehicle Electrical Power #5", [32]].id: 2261 if vehicle_electrical_power_response.pgn.id == 59392: 2262 message = ( 2263 f"Received PGN {vehicle_electrical_power_response.pgn.id} " 2264 f"({spn_types.acknowledgement(vehicle_electrical_power_response.data[0])}), " 2265 f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} ' 2266 ) 2267 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.NACK.name 2268 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2269 else: 2270 message = ( 2271 f"Received PGN {vehicle_electrical_power_response.pgn.id} " 2272 f"{vehicle_electrical_power_response.pgn.short_name}, " 2273 f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} ' 2274 ) 2275 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.WRONG_PGN.name 2276 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2277 logger.write_warning_to_html_report(message) 2278 return 2279 else: 2280 message = ( 2281 f"Battery did not respond to PGN {PGN['Vehicle Electrical Power #5', [32]].id} " 2282 f"Vehicle Electrical Power #5 request" 2283 ) 2284 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.NO_RESPONSE.name 2285 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2286 logger.write_warning_to_html_report(message) 2287 return 2288 2289 vep5_data = vehicle_electrical_power_response.data 2290 vep5_pgn = PGN["Vehicle Electrical Power #5"].data_field 2291 2292 logger.write_result_to_html_report("<span style='font-weight: bold'>Vehicle Electrical Power #5</span>") 2293 2294 for spn, elem in zip(vep5_pgn, vep5_data): 2295 if spn.name in ["Reserved", "SLI Battery Pack State of Charge"]: 2296 continue 2297 2298 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 2299 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 2300 high_value = 1.0 2301 2302 if spn.name == "SLI Battery Pack Capacity": 2303 high_value = 64255 2304 2305 if spn.name == "SLI Battery Pack Health": 2306 high_value = 125 2307 2308 if spn.name == "SLI Cranking Predicted Minimum Battery Voltage": 2309 high_value = 50 2310 2311 if not 0 <= elem <= high_value: 2312 logger.write_warning_to_html_report( 2313 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 2314 f"{cmp(elem, '<=', high_value, description)}" 2315 ) 2316 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2317 2318 vehicle_electrical_power = { 2319 "sli_battery_pack_capacity": vep5_data[1], 2320 "sli_battery_pack_health": vep5_data[2], 2321 "sli_cranking_predicted_minimum_battery_voltage": vep5_data[3], 2322 } 2323 2324 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_specification"): 2325 _GENERATED_PROFILE.vehicle_electrical_specification = "valid" 2326 2327 _GENERATED_PROFILE.vehicle_electrical_power = vehicle_electrical_power 2328 logger.write_result_to_html_report("Test Vehicle Electrical Power #5 command was successful") 2329 2330 def test_comparison(self) -> None: 2331 """ 2332 | Description | Compare if Vehicle Electrical Power #5 values matched | 2333 | :------------------- | :--------------------------------------------------------------- | 2334 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 2335 | Instructions | 1. Check if Vehicle Electrical Power #5 matched in profiles | 2336 | Pass / Fail Criteria | Pass if Vehicle Electrical Power #5 values match | 2337 | Estimated Duration | 1 second | 2338 """ 2339 2340 failed_test = False 2341 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_power") or not hasattr( 2342 _COMPARISON_PROFILE, "vehicle_electrical_power" 2343 ): 2344 message = "Nothing to compare. The profile test may have failed or been skipped" 2345 logger.write_result_to_html_report(message) 2346 pytest.skip(message) 2347 2348 if isinstance(_GENERATED_PROFILE.vehicle_electrical_power, str) or isinstance( 2349 _COMPARISON_PROFILE.vehicle_electrical_power, str 2350 ): 2351 if not isinstance( 2352 _GENERATED_PROFILE.vehicle_electrical_power, type(_COMPARISON_PROFILE.vehicle_electrical_power) 2353 ): 2354 message = "Unable to compare profiles, Vehicle Electrical Power #5 types did not match" 2355 logger.write_failure_to_html_report(message) 2356 pytest.fail(message) 2357 2358 comparison = cmp( 2359 _GENERATED_PROFILE.vehicle_electrical_power, "==", _COMPARISON_PROFILE.vehicle_electrical_power 2360 ) 2361 2362 if _GENERATED_PROFILE.vehicle_electrical_power != _COMPARISON_PROFILE.vehicle_electrical_power: 2363 message = f"Vehicle Electrical Power #5 did not match: {comparison}" 2364 logger.write_failure_to_html_report(message) 2365 pytest.fail(message) 2366 2367 message = f"Vehicle Electrical Power #5 matched: {comparison}" 2368 logger.write_result_to_html_report(message) 2369 else: 2370 2371 failed_categories = [] 2372 percent_closeness = 0.05 2373 2374 for key, value in _GENERATED_PROFILE.vehicle_electrical_power.items(): 2375 key_text = key.replace("_", " ").title() 2376 key_text = key_text.replace("Sli", "SLI") 2377 diff = abs(value - _COMPARISON_PROFILE.vehicle_electrical_power[key]) 2378 average = (value + _COMPARISON_PROFILE.vehicle_electrical_power[key]) / 2 2379 percentage = round(diff / average, 4) 2380 2381 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 2382 message = f"{key_text}: {comparison}" 2383 2384 if not math.isclose( 2385 value, 2386 _COMPARISON_PROFILE.vehicle_electrical_power[key], 2387 rel_tol=percent_closeness, 2388 ): 2389 logger.write_warning_to_html_report(message) 2390 failed_categories.append(key_text) 2391 else: 2392 logger.write_result_to_html_report(message) 2393 2394 fail_length = len(failed_categories) 2395 2396 if fail_length > 0: 2397 categories = "data values" if fail_length > 1 else "data value" 2398 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2399 logger.write_failure_to_html_report(message) 2400 failed_test = True 2401 else: 2402 logger.write_result_to_html_report( 2403 f"Vehicle Electrical Power #5 values between profiles were within {percent_closeness * 100}%" 2404 ) 2405 2406 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_specification") or not hasattr( 2407 _COMPARISON_PROFILE, "vehicle_electrical_specification" 2408 ): 2409 logger.write_warning_to_html_report("Missing specification information") 2410 else: 2411 comparison = cmp( 2412 _GENERATED_PROFILE.vehicle_electrical_specification, 2413 "==", 2414 _COMPARISON_PROFILE.vehicle_electrical_specification, 2415 ) 2416 if ( 2417 _GENERATED_PROFILE.vehicle_electrical_specification 2418 != _COMPARISON_PROFILE.vehicle_electrical_specification 2419 ): 2420 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2421 failed_test = True 2422 else: 2423 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2424 2425 if failed_test: 2426 message = "Comparisons did not match in profiles" 2427 logger.write_failure_to_html_report(message) 2428 pytest.fail(message) 2429 2430 2431class TestManufacturerCommands: 2432 """Test Manufacturer Commands are compliant with specs""" 2433 2434 def saft_commands_test(self): 2435 """Tests SAFT Manufacturer Commands""" 2436 with CANBus(BATTERY_CHANNEL) as bus: 2437 manufactured_command_request = CANFrame( 2438 destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[0] 2439 ) 2440 invalid_response = [] 2441 for address in itertools.chain( 2442 [0xFFD2], range(0xFFD4, 0xFFD9), range(0xFFDC, 0xFFDF), range(0xFFE0, 0xFFE2), [0xFFE4] 2443 ): 2444 manufactured_command_request.data = [address] 2445 default_pgn = PGN[address] 2446 pgn_name = default_pgn.name 2447 logger.write_result_to_html_report( 2448 f"<span style='font-weight: bold'>PGN {address} ({pgn_name})---</span>" 2449 ) 2450 2451 if response_frame := bus.process_call(manufactured_command_request): 2452 if not response_frame.pgn.id == default_pgn.id: 2453 2454 if response_frame.pgn.id == PGN["ACKM", [32]].id: 2455 message = ( 2456 f"Expected {address} ({pgn_name}): Received PGN {response_frame.pgn.id} " 2457 f"({spn_types.acknowledgement(response_frame.data[0])}) " 2458 ) 2459 logger.write_warning_to_html_report(message) 2460 else: 2461 logger.write_warning_to_html_report( 2462 f"Expected PGN {address} ({pgn_name}), but received " 2463 f"{response_frame.pgn.id} ({response_frame.pgn.name}). " 2464 f"Unable to complete check for command" 2465 ) 2466 invalid_response.append(f"PGN {address} ({pgn_name})") 2467 continue 2468 else: 2469 message = f"Did not receive response from PGN {address} {pgn_name}" 2470 logger.write_warning_to_html_report(message) 2471 invalid_response.append(f"PGN {address} ({pgn_name})") 2472 2473 continue 2474 2475 if response_frame.priority != default_pgn.default_priority: 2476 message = ( 2477 f"Expected priority level of {default_pgn.default_priority}" 2478 f" but got priority level {response_frame.priority} for PGN {address}, {pgn_name}" 2479 ) 2480 invalid_response.append(f"PGN {address} {pgn_name}") 2481 logger.write_warning_to_html_report(message) 2482 2483 if len(response_frame.packed_data) != 8: 2484 message = ( 2485 f"Unexpected data length for PGN {address}, {pgn_name}. Expected length of 8, " 2486 f"received {len(response_frame.packed_data)}" 2487 ) 2488 logger.write_warning_to_html_report(message) 2489 2490 not_passed_elem = [] 2491 for spn, elem in zip(default_pgn.data_field, response_frame.data): 2492 low_range = 0 2493 high_range = 3 2494 spn_name = spn.name 2495 if spn.name == "Reserved": 2496 continue 2497 2498 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 2499 2500 logger.write_result_to_html_report(f"{spn_name}: {elem}{description}") 2501 2502 if pgn_name == "Battery ECU Status": 2503 if spn_name in ("Battery Mode", "FET Array State", "SOC Mode", "Heat Reason"): 2504 high_range = 7 2505 elif spn_name == "Long-Term Fault Log Status": 2506 high_range = 15 2507 elif spn_name == "Software Part Number": 2508 high_range = 64255 2509 2510 if pgn_name in ("Battery Cell Status 1", "Battery Cell Status 2"): 2511 high_range = 6.4255 2512 2513 if pgn_name == "Battery Performance": 2514 if spn_name == "Battery Current": 2515 low_range = -82000.00 2516 high_range = 82495.35 2517 if spn_name == "Internal State of Health": 2518 low_range = -204.800 2519 high_range = 204.775 2520 2521 if pgn_name == "Battery Temperatures": 2522 if spn_name == "MCU Temperature": 2523 low_range = -40 2524 high_range = 210 2525 else: 2526 low_range = -50 2527 high_range = 200 2528 2529 if pgn_name == "Battery Balancing Circuit Info": 2530 if spn.name == "Cell Voltage Difference": 2531 high_range = 6.4255 2532 if spn_name == "Cell Voltage Sum": 2533 high_range = 104.8576 2534 2535 if pgn_name in ("Battery Cell Upper SOC", "Battery Cell Lower SOC"): 2536 low_range = -10 2537 high_range = 115 2538 2539 if pgn_name == "Battery Function Status": 2540 if spn_name == "Heater Set Point": 2541 low_range = -50 2542 high_range = 25 2543 if spn_name == "Storage Delay Time Limit": 2544 high_range = 65535 2545 if spn_name == "Last Storage Duration (Minutes)": 2546 high_range = 59 2547 if spn_name == "Last Storage Duration (Hours)": 2548 high_range = 23 2549 if spn_name == "Last Storage Duration (Days)": 2550 high_range = 31 2551 if spn_name == "Last Storage Duration (Months)": 2552 high_range = 255 2553 if spn_name == "Effective Reset Time": 2554 high_range = 60 2555 2556 if not low_range <= elem <= high_range: 2557 logger.write_warning_to_html_report( 2558 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 2559 f"{cmp(elem, '<=', high_range, description)}" 2560 ) 2561 not_passed_elem.append(spn_name) 2562 2563 if len(not_passed_elem) == 0: 2564 message = f"✅ All data fields in PGN {default_pgn.id} ({pgn_name}) met requirements" 2565 logger.write_result_to_html_report(message) 2566 2567 if len(invalid_response) > 0: 2568 message = ( 2569 f"{len(invalid_response)} SAFT Manufacturer Command{'s' if len(invalid_response) > 1 else ''} " 2570 f"failed: {', '.join(invalid_response)}" 2571 ) 2572 logger.write_warning_to_html_report(message) 2573 else: 2574 logger.write_result_to_html_report("All SAFT Manufacturer Commands passed") 2575 2576 @pytest.mark.profile 2577 def test_profile(self) -> None: 2578 """ 2579 | Description | Fingerprint Manufacturer Commands | 2580 | :------------------- | :--------------------------------------------------------------- | 2581 | GitHub Issue | turnaroundfactor/BMS-HW-Test#388 | 2582 | Instructions | 1. Send request for manufacturer command </br>\ 2583 2. Check values in data </br>\ 2584 3. Log response | 2585 | Estimated Duration | 22 seconds | 2586 """ 2587 2588 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.SAFT: 2589 self.saft_commands_test() 2590 else: 2591 message = "No known manufacturer commands to test" 2592 logger.write_result_to_html_report(message) 2593 pytest.skip(message) 2594 2595 2596# class TestUDSSession: 2597# """Tests if batteries respond to UDS Session""" 2598# 2599# @pytest.mark.profile 2600# def test_profile(self) -> None: 2601# """ 2602# | Description | Test if batteries respond to UDS Session | 2603# | :------------------- | :--------------------------------------------------------------- | 2604# | GitHub Issue | turnaroundfactor/BMS-HW-Test#294 | 2605# | Instructions | 1. Send request for UDS Session </br>\ 2606# 2. Check if battery responds </br>\ 2607# 3. Log response | 2608# | Estimated Duration | 10 minutes | 2609# """ 2610# 2611# request_data_params = ["0000", "F18C"] 2612# 2613# can_256_ids = [] 2614# for number in range(256): 2615# can_id = 0x18DA00F1 | number << 8 2616# can_256_ids.append(can_id) 2617# 2618# read_data_commands = [] 2619# # Read Data by Identifier 2620# with CANBus(BATTERY_CHANNEL) as bus: 2621# for address in itertools.chain( 2622# [0x7DF], range(0x7E0, 0x7E8), range(0x7E8, 0x7F0), [0x18DB33F1], can_256_ids 2623# ): 2624# respond_command = [] 2625# for param in request_data_params: 2626# data = f"0322{param}AAAAAAAA" 2627# bytes_data = bytes.fromhex(data) 2628# bus.send_message(can.Message(arbitration_id=address, data=bytes_data)) 2629# if response := bus.read_input(timeout=0.5): 2630# data_frame = CANFrame.decode(response.arbitration_id, response.data) 2631# if data_frame.pgn.id != PGN["ACKM"].id: 2632# logger.write_info_to_report( 2633# f"UDS Read Memory by Address response received for address {address}: {response}" 2634# ) 2635# respond_command.append(data) 2636# 2637# if respond_command: 2638# read_data_commands.append(address) 2639# 2640# if read_data_commands: 2641# logger.write_info_to_report( 2642# f"The following CAN addresses responded to UDS request: {read_data_commands}" 2643# ) 2644# else: 2645# logger.write_info_to_report("No UDS responses were received for Read Data By Identifier") 2646# 2647# # Read Memory Address 2648# read_memory_commands = [] 2649# for address in itertools.chain( 2650# [0x7DF], range(0x7E0, 0x7E8), range(0x7E8, 0x7F0), [0x18DB33F1], can_256_ids 2651# ): 2652# respond_command = [] 2653# data = "03230000AAAAAAAA" 2654# bytes_data = bytes.fromhex(data) 2655# bus.send_message(can.Message(arbitration_id=address, data=bytes_data)) 2656# if response := bus.read_input(timeout=0.5): 2657# data_frame = CANFrame.decode(response.arbitration_id, response.data) 2658# if data_frame.pgn.id != PGN["ACKM"].id: 2659# logger.write_info_to_report( 2660# f"UDS Read Memory by Address response received for address {address}: {response}" 2661# ) 2662# respond_command.append(data) 2663# 2664# if respond_command: 2665# read_memory_commands.append(address) 2666# 2667# if read_memory_commands: 2668# logger.write_info_to_report( 2669# f"The following CAN addresses responded to UDS request: {read_memory_commands}" 2670# ) 2671# else: 2672# logger.write_info_to_report("No UDS responses were received for Read Memory By Address") 2673# 2674# _GENERATED_PROFILE.uds_read_data_commands = read_data_commands 2675# _GENERATED_PROFILE.uds_read_memory_commands = read_memory_commands 2676# 2677# def test_comparison(self) -> None: 2678# """ 2679# | Description | Compare UDS responses | 2680# | :------------------- | :--------------------------------------------------------------- | 2681# | GitHub Issue | turnaroundfactor/BMS-HW-Test#294 | 2682# | Instructions | 1. Check if profiles have similar UDS responses | 2683# | Pass / Fail Criteria | Pass if profiles are closely matched | 2684# | Estimated Duration | 1 second | 2685# """ 2686# 2687# if len(_GENERATED_PROFILE.uds_read_data_commands) == 0 and 2688# len(_COMPARISON_PROFILE.uds_read_data_commands) == 0: 2689# logger.write_result_to_report("Both profiles did not receive UDS responses") 2690# 2691# if ( 2692# len(_GENERATED_PROFILE.uds_read_memory_commands) == 0 2693# and len(_COMPARISON_PROFILE.uds_read_memory_commands) == 0 2694# ): 2695# logger.write_result_to_report("Both profiles did not receive UDS responses") 2696# 2697# unique_read_data_commands = set(_GENERATED_PROFILE.uds_read_data_commands) - set( 2698# _COMPARISON_PROFILE.uds_read_data_commands 2699# ) 2700# unique_read_memory_commands = set(_GENERATED_PROFILE.uds_read_memory_commands) - set( 2701# _COMPARISON_PROFILE.uds_read_memory_commands 2702# ) 2703# 2704# if unique_read_data_commands: 2705# logger.write_warning_to_report( 2706# f"Profiles did not match read data CAN addresses: {unique_read_data_commands}" 2707# ) 2708# 2709# if unique_read_memory_commands: 2710# logger.write_warning_to_report( 2711# f"Profiles did not match read memory CAN addresses: {unique_read_memory_commands}" 2712# ) 2713# 2714# if not unique_read_data_commands and not unique_read_memory_commands: 2715# logger.write_result_to_html_report("Profiles shared similar results for UDS requests") 2716# 2717 2718 2719class TestECUInformation: 2720 """Gets information about the physical ECU and its hardware""" 2721 2722 @staticmethod 2723 def bytes_to_ascii(bs: list[float]) -> list[str]: 2724 """Converts bytes to ASCII string""" 2725 s: str = "" 2726 for b in bs: 2727 h = re.sub(r"^[^0-9a-fA-F]+$", "", f"{b:x}") 2728 try: 2729 ba = bytearray.fromhex(h)[::-1] 2730 s += ba.decode("utf-8", "ignore") 2731 s = re.sub(r"[^\x20-\x7E]", "", s) 2732 except ValueError: 2733 # NOTE: This will ignore any invalid packets (from BrenTronics) 2734 logger.write_warning_to_report(f"Skipping invalid hex: {b:x}") 2735 return list(filter(None, s.split("*"))) 2736 2737 @pytest.mark.profile 2738 def test_ecu_information(self) -> None: 2739 """ 2740 | Description | Get information from ECUID response | 2741 | :------------------- | :--------------------------------------------------------------- | 2742 | GitHub Issue | turnaroundfactor/BMS-HW-Test#395 | 2743 | Instructions | 1. Request ECUID data </br>\ 2744 2. Log returned values | 2745 | Estimated Duration | 30 seconds | 2746 """ 2747 2748 info = [] 2749 with CANBus(BATTERY_CHANNEL) as bus: 2750 ecu_request = CANFrame(pgn=PGN["Request"], data=[PGN["ECUID"].id]) 2751 if tp_cm_frame := bus.process_call(ecu_request): 2752 if tp_cm_frame is not None: 2753 if tp_cm_frame.pgn.id == PGN["TP.CM", [32]].id: 2754 data = [] 2755 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 2756 if data_frame := bus.process_call(cts_request): 2757 if data_frame is not None: 2758 if data_frame.pgn.id == PGN["TP.DT"].id: 2759 packet_count = int(tp_cm_frame.data[2]) 2760 # NOTE: This will ignore the invalid header packet from BrenTronics 2761 if _GENERATED_PROFILE.manufacturer_code != ManufacturerID.BRENTRONICS: 2762 data.append(data_frame.data[1]) 2763 packet_count -= 1 2764 for _ in range(packet_count): 2765 data_message = bus.read_input() 2766 if data_message is not None: 2767 frame = CANFrame.decode(data_message.arbitration_id, data_message.data) 2768 if frame.pgn.id == PGN["TP.DT"].id: 2769 data.append(frame.data[1]) 2770 else: 2771 Errors.unexpected_packet("TP.DT", frame) 2772 break 2773 else: 2774 Errors.no_packet("TP.DT") 2775 break 2776 info = self.bytes_to_ascii(data) 2777 eom_request = CANFrame( 2778 pgn=PGN["TP.CM"], 2779 data=[17, tp_cm_frame.data[1], packet_count, 0xFF, tp_cm_frame.data[-1]], 2780 ) 2781 if eom_frame := bus.process_call(eom_request): 2782 if eom_frame is not None: 2783 if eom_frame.pgn.id == PGN["DM15"].id: 2784 if eom_frame.data[2] == 4: # Operation Completed 2785 logger.write_info_to_report("ECUID data transfer successful") 2786 else: 2787 logger.write_warning_to_html_report("Unsuccessful EOM response") 2788 else: 2789 Errors.unexpected_packet("DM15", eom_frame) 2790 else: 2791 Errors.no_packet("DM15") 2792 else: 2793 # timeout 2794 logger.write_warning_to_report("No response after sending EOM (DM15)") 2795 else: 2796 Errors.unexpected_packet("TP.DT", data_frame) 2797 else: 2798 Errors.no_packet("TP.DT") 2799 else: 2800 message = f"Did not receive response from PGN {PGN['TP.CM', [32]].id}" 2801 logger.write_warning_to_html_report(message) 2802 _GENERATED_PROFILE.ecu = BadStates.NO_RESPONSE.name 2803 _GENERATED_PROFILE.ecu_specification = "invalid" 2804 else: 2805 Errors.unexpected_packet("TP.CM", tp_cm_frame) 2806 else: 2807 Errors.no_packet("TP.CM") 2808 else: 2809 message = f"Did not receive response from PGN {PGN['ECUID', [32]].id}" 2810 logger.write_warning_to_html_report(message) 2811 _GENERATED_PROFILE.ecu = BadStates.NO_RESPONSE.name 2812 _GENERATED_PROFILE.ecu_specification = "invalid" 2813 2814 if len(info) > 0: 2815 _GENERATED_PROFILE.ecu = { 2816 "part_number": info[0], 2817 "serial_number": info[1], 2818 "location_name": info[2], 2819 "manufacturer": info[3], 2820 "classification": info[4], 2821 } 2822 logger.write_result_to_html_report("<span style='font-weight: bold'>ECUID Information </span>") 2823 for key, value in _GENERATED_PROFILE.ecu.items(): 2824 logger.write_result_to_html_report(f"{key.strip().replace('_', ' ').title()}: {value}") 2825 _GENERATED_PROFILE.ecu_specification = "valid" 2826 2827 else: 2828 _GENERATED_PROFILE.ecu = BadStates.INVALID_RESPONSE.name 2829 logger.write_warning_to_html_report("Could not get ECU information") 2830 _GENERATED_PROFILE.ecu_specification = "invalid" 2831 2832 def test_comparison(self) -> None: 2833 """ 2834 | Description | Compare ECU Information | 2835 | :------------------- | :--------------------------------------------------------------- | 2836 | GitHub Issue | turnaroundfactor/BMS-HW-Test#395 | 2837 | Instructions | 1. Confirm identity numbers are unique for each battery | 2838 | Pass / Fail Criteria | Pass if ECU values match / serial number is unique | 2839 | Estimated Duration | 1 second | 2840 """ 2841 2842 failed_test = False 2843 if not hasattr(_GENERATED_PROFILE, "ecu") or not hasattr(_COMPARISON_PROFILE, "ecu"): 2844 logger.write_result_to_html_report("Nothing to compare. The profile test may have failed or been skipped") 2845 pytest.skip("Nothing to compare. The profile test may have failed or been skipped") 2846 2847 if isinstance(_GENERATED_PROFILE.ecu, str) or isinstance(_COMPARISON_PROFILE.ecu, str): 2848 if not isinstance(_GENERATED_PROFILE.ecu, type(_COMPARISON_PROFILE.ecu)): 2849 message = "Unable to compare profiles, ECU types did not match" 2850 logger.write_failure_to_html_report(message) 2851 pytest.fail(message) 2852 2853 comparison = cmp(_GENERATED_PROFILE.ecu, "==", _COMPARISON_PROFILE.ecu) 2854 2855 if _GENERATED_PROFILE.ecu != _COMPARISON_PROFILE.ecu: 2856 message = f"ECU did not match: {comparison}" 2857 logger.write_failure_to_html_report(message) 2858 pytest.fail(message) 2859 2860 message = f"ECU matched: {comparison}" 2861 logger.write_result_to_html_report(message) 2862 else: 2863 2864 failed_categories = [] 2865 2866 for key, value in _GENERATED_PROFILE.ecu.items(): 2867 key_text = key.replace("_", " ").title() 2868 2869 comparison = cmp(value, "==", _COMPARISON_PROFILE.ecu[key]) 2870 2871 if key == "serial_number": 2872 if value == _COMPARISON_PROFILE.ecu[key]: 2873 message = f"{key_text} matched: {comparison}" 2874 logger.write_warning_to_html_report(message) 2875 else: 2876 message = f"{key_text}: {comparison}" 2877 logger.write_result_to_html_report(message) 2878 2879 elif value != _COMPARISON_PROFILE.ecu[key]: 2880 message = f"{key_text} did not match: {comparison}" 2881 logger.write_warning_to_html_report(message) 2882 failed_categories.append(key_text) 2883 else: 2884 message = f"{key_text}: {comparison}" 2885 logger.write_result_to_html_report(message) 2886 2887 fail_length = len(failed_categories) 2888 2889 if fail_length > 0: 2890 categories = "data values" if fail_length > 1 else "data value" 2891 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2892 logger.write_failure_to_html_report(message) 2893 failed_test = True 2894 2895 if not hasattr(_GENERATED_PROFILE, "ecu_specification") or not hasattr( 2896 _COMPARISON_PROFILE, "ecu_specification" 2897 ): 2898 logger.write_warning_to_html_report("Missing specification information") 2899 else: 2900 comparison = cmp( 2901 _GENERATED_PROFILE.ecu_specification, 2902 "==", 2903 _COMPARISON_PROFILE.ecu_specification, 2904 ) 2905 if _GENERATED_PROFILE.ecu_specification != _COMPARISON_PROFILE.ecu_specification: 2906 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2907 failed_test = True 2908 else: 2909 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2910 2911 if failed_test: 2912 message = "Comparisons did not meet expectations" 2913 logger.write_failure_to_html_report(message) 2914 pytest.fail(message) 2915 else: 2916 logger.write_result_to_html_report("ECUID profile comparisons met requirements") 2917 2918 2919class TestAnalyzeTimingCommands: 2920 """Test Supported Commands & Conduct Timing Analysis""" 2921 2922 @pytest.mark.profile 2923 def test_analyze_timing_commands(self) -> None: 2924 """ 2925 | Description | Analyze timing of commands | 2926 | :------------------- | :--------------------------------------------------------------- | 2927 | GitHub Issue | turnaroundfactor/BMS-HW-Test#383 | 2928 | Instructions | 1. Make requests for supported commands </br>\ 2929 2. Conduct timing analysis </br>\ 2930 3. Log average times | 2931 | Estimated Duration | 35 seconds | 2932 """ 2933 2934 with CANBus(BATTERY_CHANNEL) as bus: 2935 j1939_transmit_commands = [0xEE00, 0xFEE6, 0xFCB6, 0xFECA, 0xFECB, 0xFE50, 0xFDC5, 0xFEDA, 0xD800, 0xE800] 2936 mil_prf_commands = [0xFF00, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08] 2937 if hasattr(_GENERATED_PROFILE, "proprietary_commands"): 2938 proprietary_commands = list(_GENERATED_PROFILE.proprietary_commands.keys()) 2939 else: 2940 proprietary_commands = [] 2941 2942 # Remove potential duplicate commands 2943 commands_list = list(dict.fromkeys(j1939_transmit_commands + mil_prf_commands + proprietary_commands)) 2944 2945 times_list: dict[int | Any, list[Any]] = {} 2946 average_times = {} 2947 no_valid_response: dict[int, bool] = {} 2948 for _ in range(0, 3): 2949 for command in commands_list: 2950 command_name = PGN[command].name 2951 if no_valid_response.get(command): 2952 continue 2953 2954 request_frame = CANFrame( 2955 destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[command] 2956 ) 2957 start_time = time.perf_counter() 2958 if response := bus.process_call(request_frame): 2959 if response.pgn.id == PGN["TP.CM", [32]].id: 2960 expected_bytes = int(response.data[1]) 2961 expected_packets = 0 2962 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.BRENTRONICS: 2963 expected_packets = int(response.data[2] + 1) 2964 else: 2965 expected_packets = int(response.data[2]) 2966 2967 rts_pgn_id = int(response.data[-1]) 2968 if command in (PGN["ECUID", [32]].id, PGN["SOFT", [32]].id): 2969 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 2970 else: 2971 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 2972 cts_request.data[1] = expected_packets 2973 cts_request.data[-1] = rts_pgn_id 2974 2975 bus.send_message(cts_request.message()) 2976 for i in range(expected_packets): 2977 data_frame = bus.read_frame() 2978 if data_frame is None: 2979 message = ( 2980 f"Received no data packet when in TP.CM protocol for command" 2981 f" {command}, {command_name}" 2982 ) 2983 logger.write_warning_to_html_report(message) 2984 if data_frame.pgn.id != PGN["TP.DT", [32]].id: 2985 message = ( 2986 f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id} " 2987 f"for PGN request {command} ({command_name}) instead" 2988 ) 2989 logger.write_warning_to_html_report(message) 2990 2991 # Send acknowledgement frame 2992 end_time = time.perf_counter() 2993 end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 2994 end_acknowledge_frame.data[1] = expected_bytes 2995 end_acknowledge_frame.data[2] = expected_packets 2996 end_acknowledge_frame.data[-1] = rts_pgn_id 2997 bus.send_message(end_acknowledge_frame.message()) 2998 else: 2999 end_time = time.perf_counter() 3000 3001 if response.pgn.id in (command, PGN["TP.CM", [32]].id): 3002 if not times_list.get(command): 3003 times_list[command] = [end_time - start_time] 3004 else: 3005 times_list[command].append(end_time - start_time) 3006 else: 3007 if response.pgn.id == PGN["ACKM", [32]].id: 3008 message = ( 3009 f"PGN {command} ({command_name}): Received PGN {response.pgn.id} " 3010 f"({spn_types.acknowledgement(response.data[0])}) " 3011 ) 3012 average_times[command] = ( 3013 f"PGN {response.pgn.id} ({spn_types.acknowledgement(response.data[0])})" 3014 ) 3015 else: 3016 message = ( 3017 f"PGN {command} ({command_name}): Received PGN {response.pgn.id} " 3018 f"({response.pgn.name})." 3019 ) 3020 average_times[command] = f"PGN {response.pgn.id} ({response.pgn.name})" 3021 logger.write_warning_to_html_report(message) 3022 no_valid_response[command] = True 3023 continue 3024 else: 3025 logger.write_warning_to_html_report( 3026 f"Did not receive any response for PGN {command} ({command_name})" 3027 ) 3028 no_valid_response[command] = True 3029 average_times[command] = "No response" 3030 continue 3031 3032 for pgn, item in times_list.items(): 3033 times = item 3034 average = sum(times) / len(times) 3035 average_times[pgn] = average 3036 message = f"PGN {pgn} ({PGN[pgn].name}) average time: {average:.6f} seconds" 3037 logger.write_result_to_html_report(message) 3038 3039 _GENERATED_PROFILE.average_times = average_times 3040 3041 def test_comparison(self) -> None: 3042 """ 3043 | Description | Compare average time values | 3044 | :------------------- | :--------------------------------------------------------------- | 3045 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 3046 | Instructions | 1. Check if profiles complete commands in approx. same time | 3047 | Pass / Fail Criteria | Pass if profiles time completion are closely matched | 3048 | Estimated Duration | 1 second | 3049 """ 3050 3051 if not hasattr(_GENERATED_PROFILE, "average_times") or not hasattr(_COMPARISON_PROFILE, "average_times"): 3052 message = "Nothing to compare. The profile test may have failed or been skipped" 3053 logger.write_failure_to_html_report(message) 3054 pytest.fail(message) 3055 3056 generated_averages = _GENERATED_PROFILE.average_times 3057 comparison_averages = _COMPARISON_PROFILE.average_times 3058 3059 averages_not_close = [] 3060 missing_commands = [] 3061 percent_closeness = 0.50 3062 for pgn in generated_averages: 3063 if not comparison_averages.get(str(pgn)): 3064 logger.write_warning_to_html_report(f"Missing PGN {pgn} ({PGN[pgn].name}) from comparison profile") 3065 missing_commands.append(pgn) 3066 continue 3067 3068 if isinstance(generated_averages[pgn], str) or isinstance(comparison_averages[str(pgn)], str): 3069 if generated_averages[pgn] != comparison_averages[str(pgn)]: 3070 logger.write_warning_to_html_report( 3071 f"PGN {pgn} ({PGN[pgn].name}): {generated_averages[pgn]} ≠ {comparison_averages[str(pgn)]}" 3072 ) 3073 else: 3074 logger.write_result_to_html_report( 3075 f"PGN {pgn} ({PGN[pgn].name}): {generated_averages[pgn]} = {comparison_averages[str(pgn)]}" 3076 ) 3077 continue 3078 3079 diff = abs(comparison_averages[str(pgn)] - generated_averages[pgn]) 3080 average = (generated_averages[pgn] + comparison_averages[str(pgn)]) / 2 3081 percentage = round((diff / average), 4) 3082 comparison = cmp( 3083 percentage * 100, 3084 "<=", 3085 percent_closeness * 100, 3086 "%", 3087 ) 3088 3089 if not math.isclose( 3090 generated_averages[pgn], 3091 comparison_averages[str(pgn)], 3092 rel_tol=percent_closeness, 3093 ): 3094 logger.write_warning_to_html_report(f"PGN {pgn} ({PGN[pgn].name}) average percentage: {comparison}") 3095 averages_not_close.append(pgn) 3096 else: 3097 logger.write_result_to_html_report(f"PGN {pgn} ({PGN[pgn].name}) average percentage: {comparison}") 3098 3099 length_not_close = len(averages_not_close) 3100 if length_not_close > 0: 3101 logger.write_failure_to_html_report( 3102 f"{length_not_close} comparison{'s' if length_not_close > 1 else ''} " 3103 f"did not meet expectations. See warnings above for more details." 3104 ) 3105 pytest.fail("Some comparison times were not within range") 3106 3107 if len(missing_commands) > 0: 3108 logger.write_failure_to_html_report(f"{len(missing_commands)} PGNs were missing from comparison profile") 3109 3110 logger.write_result_to_html_report( 3111 f"All PGN Average times for profiles were within expected range of {percent_closeness * 100}%" 3112 ) 3113 3114 3115def memory_test(mode: Modes) -> list[int]: 3116 """Memory tester helper""" 3117 found_addresses = [] 3118 with CANBus(BATTERY_CHANNEL) as bus: 3119 read_request_frame = CANFrame(pgn=PGN["DM14"], data=[1, 1, 1, 0, 0, 0, 0, 0]) 3120 read_request_frame.data[2] = mode 3121 addresses = [0, 1107296256, 2147483648, 4294966271] # 0x0, 0x42000000, 0x80000000, 0xfffffbff 3122 for low_address in addresses: 3123 found = 0 3124 high_address = low_address + (1024 if FULL_MEMORY_TESTS else 16) 3125 for i in range(low_address, high_address): 3126 read_request_frame.data[5] = low_address 3127 if response_frame := bus.process_call(read_request_frame, timeout=1): # NOTE: SAFT times out 3128 if response_frame is not None: 3129 logger.write_info_to_report( 3130 f"Address {i} responded with PGN {response_frame.pgn.id} " 3131 f"({response_frame.pgn.short_name}) - status is: " 3132 f"{spn_types.dm15_status(response_frame.data[3])}" 3133 ) 3134 if response_frame.pgn.id == PGN["DM15"].id: 3135 if response_frame.data[5] != 258: # Invalid Length 3136 if response_frame.data[3] == 0: 3137 found_addresses.append(i) 3138 found += 1 3139 else: 3140 Errors.unexpected_packet("DM15", response_frame) 3141 break 3142 else: 3143 Errors.no_packet("DM15") 3144 break 3145 else: 3146 # timeout 3147 pass 3148 verb = "" 3149 match mode: 3150 case Modes.READ: 3151 verb = "readable" 3152 case Modes.WRITE: 3153 verb = "writable" 3154 case Modes.ERASE: 3155 verb = "erasable" 3156 case Modes.BOOT: 3157 verb = "boot load" 3158 message = ( 3159 f"Found {found} {verb} successful address(es) in memory ranges" 3160 f" {hex(low_address)}-{hex(high_address)}" 3161 ) 3162 logger.write_result_to_html_report(message) 3163 3164 if len(found_addresses) > 0: 3165 logger.write_result_to_html_report( 3166 f"Found {len(found_addresses)} {verb} memory address(es) out of " 3167 f"{4096 if FULL_MEMORY_TESTS else 64} possible addresses" 3168 ) 3169 else: 3170 message = f"Found 0 {verb} memory addresses out of {4096 if FULL_MEMORY_TESTS else 64} possible addresses" 3171 logger.write_warning_to_html_report(message) 3172 return found_addresses 3173 3174 3175class TestMemoryRead: 3176 """This will test the read capability of the memory""" 3177 3178 @pytest.mark.profile 3179 def test_read(self) -> None: 3180 """ 3181 | Description | Try to read from different memory locations | 3182 | :------------------- | :--------------------------------------------------------------- | 3183 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3184 turnaroundfactor/BMS-HW-Test#380 | 3185 | Instructions | 1. Request memory read </br>\ 3186 2. Log any successful addresses | 3187 | Estimated Duration | 85 seconds | 3188 """ 3189 3190 _GENERATED_PROFILE.read_addresses = memory_test(Modes.READ) 3191 3192 def test_comparison(self) -> None: 3193 """ 3194 | Description | Compare read results | 3195 | :------------------- | :--------------------------------------------------------------- | 3196 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3197 turnaroundfactor/BMS-HW-Test#380 | 3198 | Instructions | Confirm no new memory addresses are readable | 3199 | Pass / Fail Criteria | Pass if no new readable memory addresses | 3200 | Estimated Duration | 1 second | 3201 """ 3202 3203 if hasattr(_GENERATED_PROFILE, "read_addresses") and hasattr(_COMPARISON_PROFILE, "read_addresses"): 3204 new_addresses = set(_GENERATED_PROFILE.read_addresses) - set(_COMPARISON_PROFILE.read_addresses) 3205 if new_addresses: 3206 message = ( 3207 f"{len(new_addresses)} Different readable memory address(es) found: " 3208 f"{', '.join(map(str, new_addresses))}" 3209 ) 3210 logger.write_failure_to_html_report(message) 3211 pytest.fail(message) 3212 3213 logger.write_result_to_html_report("Profiles had matching readable memory address responses") 3214 3215 else: 3216 message = "Nothing to compare. The profile test may have failed or been skipped" 3217 logger.write_result_to_html_report(message) 3218 pytest.skip(message) 3219 3220 3221class TestMemoryWrite: 3222 """This will test the write capability of the memory""" 3223 3224 @pytest.mark.profile 3225 def test_write(self) -> None: 3226 """ 3227 | Description | Try to write to different memory locations | 3228 | :------------------- | :--------------------------------------------------------------- | 3229 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3230 turnaroundfactor/BMS-HW-Test#380 | 3231 | Instructions | 1. Request memory write </br>\ 3232 2. Log any successful addresses | 3233 | Estimated Duration | 85 seconds | 3234 """ 3235 3236 _GENERATED_PROFILE.write_addresses = memory_test(Modes.WRITE) 3237 3238 def test_comparison(self) -> None: 3239 """ 3240 | Description | Compare write results | 3241 | :------------------- | :--------------------------------------------------------------- | 3242 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3243 turnaroundfactor/BMS-HW-Test#380 | 3244 | Instructions | Confirm no new memory addresses are writable | 3245 | Pass / Fail Criteria | Pass if no new writable memory addresses | 3246 | Estimated Duration | 1 second | 3247 """ 3248 3249 if hasattr(_GENERATED_PROFILE, "write_addresses") and hasattr(_COMPARISON_PROFILE, "write_addresses"): 3250 new_addresses = set(_GENERATED_PROFILE.write_addresses) - set(_COMPARISON_PROFILE.write_addresses) 3251 if new_addresses: 3252 message = ( 3253 f"{len(new_addresses)} Different writable memory address(es) found: " 3254 f"{', '.join(map(str, new_addresses))}" 3255 ) 3256 logger.write_failure_to_html_report(message) 3257 pytest.fail(message) 3258 3259 logger.write_result_to_html_report("Profiles had matching writable memory address responses") 3260 3261 else: 3262 message = "Nothing to compare. The profile test may have failed or been skipped" 3263 logger.write_result_to_html_report(message) 3264 pytest.skip(message) 3265 3266 3267class TestMemoryErase: 3268 """This will test the erase capability of the memory""" 3269 3270 @pytest.mark.profile 3271 def test_erase(self) -> None: 3272 """ 3273 | Description | Try to erase different memory locations | 3274 | :------------------- | :--------------------------------------------------------------- | 3275 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3276 turnaroundfactor/BMS-HW-Test#380 | 3277 | Instructions | 1. Request memory erase </br>\ 3278 2. Log any successful addresses | 3279 | Estimated Duration | 85 seconds | 3280 """ 3281 3282 _GENERATED_PROFILE.erase_addresses = memory_test(Modes.ERASE) 3283 3284 def test_comparison(self) -> None: 3285 """ 3286 | Description | Compare erase results | 3287 | :------------------- | :--------------------------------------------------------------- | 3288 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3289 turnaroundfactor/BMS-HW-Test#380 | 3290 | Instructions | Confirm no new memory addresses are erasable | 3291 | Pass / Fail Criteria | Pass if no new erasable memory addresses | 3292 | Estimated Duration | 1 second | 3293 """ 3294 3295 if hasattr(_GENERATED_PROFILE, "erase_addresses") and hasattr(_COMPARISON_PROFILE, "erase_addresses"): 3296 new_addresses = set(_GENERATED_PROFILE.erase_addresses) - set(_COMPARISON_PROFILE.erase_addresses) 3297 if new_addresses: 3298 message = ( 3299 f"{len(new_addresses)} Different erasable memory address(es) found: " 3300 f"{', '.join(map(str, new_addresses))}" 3301 ) 3302 logger.write_failure_to_html_report(message) 3303 pytest.fail(message) 3304 3305 logger.write_result_to_html_report("Profiles had matching erasable memory address responses") 3306 3307 else: 3308 message = "Nothing to compare. The profile test may have failed or been skipped" 3309 logger.write_result_to_html_report(message) 3310 pytest.skip(message) 3311 3312 3313class TestMemoryBootLoad: 3314 """This will test the boot load capability of the memory""" 3315 3316 @pytest.mark.profile 3317 def test_boot(self) -> None: 3318 """ 3319 | Description | Try to boot load from different memory locations | 3320 | :------------------- | :--------------------------------------------------------------- | 3321 | GitHub Issue | turnaroundfactor/BMS-HW-Test#381 </br>\ 3322 turnaroundfactor/BMS-HW-Test#382 | 3323 | Instructions | 1. Request memory boot load </br>\ 3324 2. Log any successful addresses | 3325 | Estimated Duration | 85 seconds | 3326 """ 3327 3328 _GENERATED_PROFILE.boot_addresses = memory_test(Modes.BOOT) 3329 3330 def test_comparison(self) -> None: 3331 """ 3332 | Description | Compare boot load results | 3333 | :------------------- | :--------------------------------------------------------------- | 3334 | GitHub Issue | turnaroundfactor/BMS-HW-Test#381 </br>\ 3335 turnaroundfactor/BMS-HW-Test#382 | 3336 | Instructions | Confirm no new memory addresses are boot load capable | 3337 | Pass / Fail Criteria | Pass if no new boot load memory addresses | 3338 | Estimated Duration | 1 second | 3339 """ 3340 3341 if hasattr(_GENERATED_PROFILE, "boot_addresses") and hasattr(_COMPARISON_PROFILE, "boot_addresses"): 3342 new_addresses = set(_GENERATED_PROFILE.boot_addresses) - set(_COMPARISON_PROFILE.boot_addresses) 3343 if new_addresses: 3344 message = ( 3345 f"{len(new_addresses)} Different boot memory address(es) found: " 3346 f"{', '.join(map(str, new_addresses))}" 3347 ) 3348 logger.write_failure_to_html_report(message) 3349 pytest.fail(message) 3350 3351 logger.write_result_to_html_report("Profiles had matching boot memory address responses") 3352 3353 else: 3354 message = "Nothing to compare. The profile test may have failed or been skipped" 3355 logger.write_result_to_html_report(message) 3356 pytest.skip(message)
Channel identification. Expected type is backend dependent.
Whether to use a soft or hard reset before each test.
The name of the profile to compare against. If this is not provided, a profile json will be generated.
Short or long memory tests.
Path for profile logs.
79class ManufacturerID(IntEnum): 80 """Enum to hold Manufacturer IDs.""" 81 82 SAFT = 269 # NOTE: unused 83 BRENTRONICS = 822
Enum to hold Manufacturer IDs.
Inherited Members
- enum.Enum
- name
- value
- builtins.int
- conjugate
- bit_length
- bit_count
- to_bytes
- from_bytes
- as_integer_ratio
- is_integer
- real
- imag
- numerator
- denominator
86class Modes(float, Enum): 87 """Enum for Command modes""" 88 89 ERASE = 0 90 READ = 1 91 WRITE = 2 92 BOOT = 6 93 EDCP = 7
Enum for Command modes
Inherited Members
- enum.Enum
- name
- value
- builtins.float
- conjugate
- as_integer_ratio
- fromhex
- hex
- is_integer
- real
- imag
96class BadStates(Enum): 97 """Enum for Bad Test Response States""" 98 99 NO_RESPONSE = 0 100 WRONG_PGN = 1 101 NACK = 2 102 WRONG_PACKETS = 3 103 INVALID_RESPONSE = 4 104 SKIPPED_TEST = 5
Enum for Bad Test Response States
Inherited Members
- enum.Enum
- name
- value
107class Errors: 108 """Holds different error messages""" 109 110 @staticmethod 111 def timeout() -> None: 112 """Prints message when connection times out""" 113 message = "Could not locate 6T" 114 logger.write_critical_to_report(message) 115 pytest.exit(message) 116 117 @staticmethod 118 def unexpected_packet(expected: str, frame: CANFrame) -> None: 119 """Prints message when unexpected packet is received""" 120 logger.write_warning_to_report(f"Expected {expected}, got {frame.pgn.short_name}") 121 122 @staticmethod 123 def no_packet(expected: str) -> None: 124 """Prints message when no packet is received""" 125 logger.write_warning_to_report(f"Expected {expected}, got None")
Holds different error messages
110 @staticmethod 111 def timeout() -> None: 112 """Prints message when connection times out""" 113 message = "Could not locate 6T" 114 logger.write_critical_to_report(message) 115 pytest.exit(message)
Prints message when connection times out
117 @staticmethod 118 def unexpected_packet(expected: str, frame: CANFrame) -> None: 119 """Prints message when unexpected packet is received""" 120 logger.write_warning_to_report(f"Expected {expected}, got {frame.pgn.short_name}")
Prints message when unexpected packet is received
131def cmp( 132 a: float | str, 133 sign: Literal["<", "<=", ">", ">=", "=="], 134 b: float | str, 135 unit_a: str = "", 136 unit_b: str = "", 137 form: str = "", 138) -> str: 139 """Generate a formatted string based on a comparison.""" 140 if not unit_b: 141 unit_b = unit_a 142 143 if isinstance(a, str) or isinstance(b, str): 144 return f"{a:{form}}{unit_a} {('≠', '=')[a == b]} {b:{form}}{unit_b}" 145 146 sign_str = { 147 "<": ("≮", "<")[a < b], 148 "<=": ("≰", "≤")[a <= b], 149 ">": ("≯", ">")[a > b], 150 ">=": ("≱", "≥")[a >= b], 151 "==": ("≠", "=")[a == b], 152 } 153 154 return f"{a:{form}}{unit_a} {sign_str[sign]} {b:{form}}{unit_b}"
Generate a formatted string based on a comparison.
157@pytest.fixture(scope="session", autouse=True) 158def generate_profile(): 159 """Generate a json object for the profile after all tests complete.""" 160 global _COMPARISON_PROFILE 161 162 if PROFILE: 163 _COMPARISON_PROFILE = SimpleNamespace(**json.loads(Path(PROFILE).read_text(encoding="UTF-8"))) 164 165 yield # Begin session 166 167 if not PROFILE: 168 try: 169 manufacturer_name = "".join(filter(str.isalnum, _GENERATED_PROFILE.manufacturer_name)) 170 battery_id = f"{_GENERATED_PROFILE.id:06X}" 171 except AttributeError: 172 logger.write_error_to_report("Could not write profile: No name or ID") 173 else: 174 profile_path = (Path(LOG_PATH) / "profile") / f"{manufacturer_name}_6T_{battery_id}.json" 175 profile_path.parent.mkdir(parents=True, exist_ok=True) 176 profile_path.write_text(json.dumps(_GENERATED_PROFILE, default=vars, indent=4), encoding="UTF-8") 177 logger.write_info_to_report(f"New Profile: {profile_path.absolute()}")
Generate a json object for the profile after all tests complete.
180@pytest.fixture(scope="class", autouse=True) 181def reset_test_environment(): 182 """Before each test class, reset the 6T.""" 183 184 try: 185 power_cycle_frame = CANFrame( 186 destination_address=_GENERATED_PROFILE.address, 187 pgn=PGN["PropA"], 188 data=[0, 0, 1, 1, 1, not HARD_RESET, 1, 3, 0, -1], 189 ) 190 maintenance_mode_frame = CANFrame( 191 destination_address=_GENERATED_PROFILE.address, 192 pgn=PGN["PropA"], 193 data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1], 194 ) 195 factory_reset_frame = CANFrame( 196 destination_address=_GENERATED_PROFILE.address, 197 pgn=PGN["PropA"], 198 data=[1, 0, 0, -1, 3, 0xF, 3, 0, 0x1F, -1], 199 ) 200 with CANBus(BATTERY_CHANNEL) as bus: 201 logger.write_info_to_report("Power-Cycling 6T") 202 bus.process_call(power_cycle_frame) 203 time.sleep(10) 204 logger.write_info_to_report("Entering maintenance mode") 205 bus.process_call(maintenance_mode_frame) 206 logger.write_info_to_report("Factory resetting 6T") 207 bus.process_call(factory_reset_frame) 208 time.sleep(10) 209 except AttributeError: # Battery has not yet been found 210 return
Before each test class, reset the 6T.
213@pytest.fixture(autouse=True) 214def check_mode(request): 215 """Skip comparison tests and output a profile if no comparison file is provided.""" 216 if not PROFILE and request.node.get_closest_marker("profile") is None: 217 pytest.skip('Mode is "Profile Generation".')
Skip comparison tests and output a profile if no comparison file is provided.
220class TestLocate6T: 221 """Scan for 6T battery.""" 222 223 MAX_ATTEMPTS = 12 # 2 minutes worth of attempts 224 225 @pytest.mark.profile 226 def test_profile(self) -> None: 227 """ 228 | Description | Scan the bus for devices | 229 | :------------------- | :--------------------------------------------------------------- | 230 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 231 | Instructions | 1. Request the names of everyone on the bus </br>\ 232 2. Use the response data for all communication | 233 | Estimated Duration | 1 second | 234 """ 235 name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 236 with CANBus(BATTERY_CHANNEL) as bus: 237 for attempt in range(self.MAX_ATTEMPTS): 238 with suppress(IndexError): 239 if name_frame := bus.process_call(name_request): 240 # Save responses to profile 241 _GENERATED_PROFILE.id = int(name_frame.data[0]) 242 _GENERATED_PROFILE.manufacturer_code = name_frame.data[1] 243 if (mfg_name := spn_types.manufacturer_id(name_frame.data[1])) == "Reserved": 244 mfg_name = f"Unknown ({name_frame.data[1]})" 245 _GENERATED_PROFILE.manufacturer_name = mfg_name 246 _GENERATED_PROFILE.address = name_frame.source_address 247 break 248 logger.write_warning_to_html_report(f"Failed to locate 6T. Retrying, attempt {attempt + 1}") 249 time.sleep(10) 250 else: 251 message = "Could not locate 6T" 252 logger.write_warning_to_html_report(message) 253 pytest.exit(message) 254 255 # Log results 256 printable_id = "".join( 257 chr(i) if chr(i).isprintable() else "." for i in _GENERATED_PROFILE.id.to_bytes(3, byteorder="big") 258 ) 259 logger.write_result_to_html_report( 260 f"Found {_GENERATED_PROFILE.manufacturer_name} 6T at address {_GENERATED_PROFILE.address} " 261 f"(ID: {_GENERATED_PROFILE.id:06X}, ASCII ID: {printable_id})" 262 )
Scan for 6T battery.
225 @pytest.mark.profile 226 def test_profile(self) -> None: 227 """ 228 | Description | Scan the bus for devices | 229 | :------------------- | :--------------------------------------------------------------- | 230 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 231 | Instructions | 1. Request the names of everyone on the bus </br>\ 232 2. Use the response data for all communication | 233 | Estimated Duration | 1 second | 234 """ 235 name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 236 with CANBus(BATTERY_CHANNEL) as bus: 237 for attempt in range(self.MAX_ATTEMPTS): 238 with suppress(IndexError): 239 if name_frame := bus.process_call(name_request): 240 # Save responses to profile 241 _GENERATED_PROFILE.id = int(name_frame.data[0]) 242 _GENERATED_PROFILE.manufacturer_code = name_frame.data[1] 243 if (mfg_name := spn_types.manufacturer_id(name_frame.data[1])) == "Reserved": 244 mfg_name = f"Unknown ({name_frame.data[1]})" 245 _GENERATED_PROFILE.manufacturer_name = mfg_name 246 _GENERATED_PROFILE.address = name_frame.source_address 247 break 248 logger.write_warning_to_html_report(f"Failed to locate 6T. Retrying, attempt {attempt + 1}") 249 time.sleep(10) 250 else: 251 message = "Could not locate 6T" 252 logger.write_warning_to_html_report(message) 253 pytest.exit(message) 254 255 # Log results 256 printable_id = "".join( 257 chr(i) if chr(i).isprintable() else "." for i in _GENERATED_PROFILE.id.to_bytes(3, byteorder="big") 258 ) 259 logger.write_result_to_html_report( 260 f"Found {_GENERATED_PROFILE.manufacturer_name} 6T at address {_GENERATED_PROFILE.address} " 261 f"(ID: {_GENERATED_PROFILE.id:06X}, ASCII ID: {printable_id})" 262 )
| Description | Scan the bus for devices |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#396 |
| Instructions | 1. Request the names of everyone on the bus 2. Use the response data for all communication |
| Estimated Duration | 1 second |
265class TestProprietaryCommands: 266 """Scan for proprietary commands""" 267 268 @pytest.mark.profile 269 def test_profile(self) -> None: 270 """ 271 | Description | Scan for proprietary commands | 272 | :------------------- | :--------------------------------------------------------------- | 273 | GitHub Issue | turnaroundfactor/BMS-HW-Test#293 | 274 | Instructions | 1. Request data from all 512 proprietary commands </br>\ 275 2. Log any responses | 276 | Estimated Duration | 47 seconds | 277 | Note | Proprietary commands are unique to each battery type or \ 278 manufacturer, which makes them useful for fingerprinting. \ 279 Especially if undocumented. | 280 """ 281 ack_responses: dict[int, CANFrame] = {} # address: response 282 proprietary_request = CANFrame(destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[0]) 283 284 with CANBus(BATTERY_CHANNEL) as bus: 285 # Check all proprietary address ranges (0xEF00-FF00) and (0xFF00, 0x10000) 286 for address in itertools.chain(range(0xEF00, 0xF000), range(0xFF00, 0x10000)): 287 proprietary_request.data = [address] 288 if response_frame := bus.process_call(proprietary_request): # Log if response is not NACK 289 if not (response_frame.pgn.id == PGN["Acknowledgement"].id and response_frame.data[0] == 1): 290 ack_responses[address] = response_frame 291 logger.write_info_to_report(f"Got response at address {address:04X}: {response_frame}") 292 293 # Check conformity to MIL-PRF commands (EF00, FF00-08) 294 # TODO(JA): Also check format (or should that be a separate test?) 295 milprf_commands = set(range(0xFF00, 0xFF09)) 296 if milprf := milprf_commands - set(ack_responses): 297 logger.write_result_to_html_report(f"Missing MIL-PRF commands: {', '.join(map(str, milprf))}") 298 299 # Save responses to profile 300 logger.write_result_to_html_report( 301 f"Found {len(ack_responses)} commands at: {', '.join(map(lambda i: f'{i:X}', ack_responses))}" 302 ) 303 _GENERATED_PROFILE.proprietary_commands = ack_responses 304 305 def test_comparison(self) -> None: 306 """ 307 | Description | Compare proprietary commands | 308 | :------------------- | :--------------------------------------------------------------- | 309 | GitHub Issue | turnaroundfactor/BMS-HW-Test#293 | 310 | Instructions | 1. Confirm no new commands exit and no commands are missing </br>\ 311 2. Check contents of MIL-PRF range is valid | 312 | Pass / Fail Criteria | Pass if we found all commands, and saw no new or malformed ones | 313 | Estimated Duration | 1 second | 314 """ 315 316 comparison_commands = set(map(int, _COMPARISON_PROFILE.proprietary_commands)) # Converted to str by json 317 missing_commands = comparison_commands - set(_GENERATED_PROFILE.proprietary_commands) 318 new_commands = set(_GENERATED_PROFILE.proprietary_commands) - comparison_commands 319 320 # Check results 321 logger.write_result_to_html_report(f"Missing: {len(missing_commands)}, New: {len(new_commands)}") 322 if missing_commands or new_commands: 323 logger.write_warning_to_html_report(f"Missing commands: {', '.join(map(str, missing_commands))}") 324 logger.write_warning_to_html_report(f"New Commands: {', '.join(map(str, new_commands))}") 325 logger.write_failure_to_html_report("Missing or new commands present.") 326 pytest.fail("Missing or new commands present.")
Scan for proprietary commands
268 @pytest.mark.profile 269 def test_profile(self) -> None: 270 """ 271 | Description | Scan for proprietary commands | 272 | :------------------- | :--------------------------------------------------------------- | 273 | GitHub Issue | turnaroundfactor/BMS-HW-Test#293 | 274 | Instructions | 1. Request data from all 512 proprietary commands </br>\ 275 2. Log any responses | 276 | Estimated Duration | 47 seconds | 277 | Note | Proprietary commands are unique to each battery type or \ 278 manufacturer, which makes them useful for fingerprinting. \ 279 Especially if undocumented. | 280 """ 281 ack_responses: dict[int, CANFrame] = {} # address: response 282 proprietary_request = CANFrame(destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[0]) 283 284 with CANBus(BATTERY_CHANNEL) as bus: 285 # Check all proprietary address ranges (0xEF00-FF00) and (0xFF00, 0x10000) 286 for address in itertools.chain(range(0xEF00, 0xF000), range(0xFF00, 0x10000)): 287 proprietary_request.data = [address] 288 if response_frame := bus.process_call(proprietary_request): # Log if response is not NACK 289 if not (response_frame.pgn.id == PGN["Acknowledgement"].id and response_frame.data[0] == 1): 290 ack_responses[address] = response_frame 291 logger.write_info_to_report(f"Got response at address {address:04X}: {response_frame}") 292 293 # Check conformity to MIL-PRF commands (EF00, FF00-08) 294 # TODO(JA): Also check format (or should that be a separate test?) 295 milprf_commands = set(range(0xFF00, 0xFF09)) 296 if milprf := milprf_commands - set(ack_responses): 297 logger.write_result_to_html_report(f"Missing MIL-PRF commands: {', '.join(map(str, milprf))}") 298 299 # Save responses to profile 300 logger.write_result_to_html_report( 301 f"Found {len(ack_responses)} commands at: {', '.join(map(lambda i: f'{i:X}', ack_responses))}" 302 ) 303 _GENERATED_PROFILE.proprietary_commands = ack_responses
| Description | Scan for proprietary commands |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#293 |
| Instructions | 1. Request data from all 512 proprietary commands 2. Log any responses |
| Estimated Duration | 47 seconds |
| Note | Proprietary commands are unique to each battery type or manufacturer, which makes them useful for fingerprinting. Especially if undocumented. |
305 def test_comparison(self) -> None: 306 """ 307 | Description | Compare proprietary commands | 308 | :------------------- | :--------------------------------------------------------------- | 309 | GitHub Issue | turnaroundfactor/BMS-HW-Test#293 | 310 | Instructions | 1. Confirm no new commands exit and no commands are missing </br>\ 311 2. Check contents of MIL-PRF range is valid | 312 | Pass / Fail Criteria | Pass if we found all commands, and saw no new or malformed ones | 313 | Estimated Duration | 1 second | 314 """ 315 316 comparison_commands = set(map(int, _COMPARISON_PROFILE.proprietary_commands)) # Converted to str by json 317 missing_commands = comparison_commands - set(_GENERATED_PROFILE.proprietary_commands) 318 new_commands = set(_GENERATED_PROFILE.proprietary_commands) - comparison_commands 319 320 # Check results 321 logger.write_result_to_html_report(f"Missing: {len(missing_commands)}, New: {len(new_commands)}") 322 if missing_commands or new_commands: 323 logger.write_warning_to_html_report(f"Missing commands: {', '.join(map(str, missing_commands))}") 324 logger.write_warning_to_html_report(f"New Commands: {', '.join(map(str, new_commands))}") 325 logger.write_failure_to_html_report("Missing or new commands present.") 326 pytest.fail("Missing or new commands present.")
| Description | Compare proprietary commands |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#293 |
| Instructions | 1. Confirm no new commands exit and no commands are missing 2. Check contents of MIL-PRF range is valid |
| Pass / Fail Criteria | Pass if we found all commands, and saw no new or malformed ones |
| Estimated Duration | 1 second |
329class TestNameInformation: 330 """Check Unique ID (Identity Number) field in Address Claimed message""" 331 332 @pytest.mark.profile 333 def test_profile(self) -> None: 334 """ 335 | Description | Confirm information in NAME (address claimed) matches \ 336 expected value. | 337 | :------------------- | :--------------------------------------------------------------- | 338 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 339 | Instructions | 1. Request Address Claimed data </br>\ 340 2. Log returned values </br>\ 341 3. Validate if values meet spec requirements | 342 | Estimated Duration | 21 seconds | 343 """ 344 345 name_values = {} 346 347 name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 348 with CANBus(BATTERY_CHANNEL) as bus: 349 if name_frame := bus.process_call(name_request): 350 name_values["identity_number"] = int(name_frame.data[0]) 351 352 if name_values["identity_number"] != _GENERATED_PROFILE.id: 353 message = f"Identity Number {name_values} does not match Profile ID: {_GENERATED_PROFILE.id}" 354 logger.write_warning_to_html_report(message) 355 356 name_values["manufacturer_code"] = name_frame.data[1] 357 name_values["ecu_instance"] = name_frame.data[2] 358 name_values["function_instance"] = name_frame.data[3] 359 name_values["function"] = name_frame.data[4] 360 name_values["reserved"] = name_frame.data[5] 361 name_values["vehicle_system"] = name_frame.data[6] 362 name_values["vehicle_system_instance"] = name_frame.data[7] 363 name_values["industry_group"] = name_frame.data[8] 364 name_values["arbitrary_address_capable"] = name_frame.data[9] 365 366 for spn, elem in zip(PGN["Address Claimed"].data_field, name_frame.data): 367 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 368 logger.write_info_to_report(f"{spn.name}: {elem} {description}") 369 370 if spn.name in ("Manufacturer Code", "Reserved"): 371 continue 372 373 high_range = 0 374 if spn.name == "Identity Number": 375 high_range = 2097151 376 377 if spn.name == "ECU Instance": 378 high_range = 7 379 380 if spn.name == "Function Instance": 381 high_range = 31 382 383 if spn.name == "Function": 384 high_range = 254 385 386 if spn.name == "Vehicle System Instance": 387 high_range = 15 388 389 if spn.name == "Industry Group": 390 high_range = 7 391 392 if spn.name == "Arbitrary Address Capable": 393 high_range = 1 394 395 if spn.name == "Vehicle System": 396 if ( 397 name_values["vehicle_system"] != 127 398 and name_values["vehicle_system"] != 0 399 and name_values["vehicle_system"] != name_values["vehicle_system_instance"] 400 ): 401 logger.write_warning_to_html_report( 402 f"Vehicle System {elem} is not 127, 0, or the same value as " f"Vehicle System Instance" 403 ) 404 _GENERATED_PROFILE.name_violation = "invalid" 405 else: 406 _GENERATED_PROFILE.name_violation = "valid" 407 408 else: 409 if not 0 <= elem <= high_range: 410 logger.write_warning_to_html_report( 411 f"{spn.name}: {cmp(elem, '>=', 0)} and " f"{cmp(elem, '<=', high_range)}" 412 ) 413 _GENERATED_PROFILE.name_violation = "invalid" 414 else: 415 _GENERATED_PROFILE.name_violation = "valid" 416 417 _GENERATED_PROFILE.name_properties = name_values 418 419 else: 420 message = f"No response to PGN {PGN['Address Claimed', [32]].id} (Address Claimed)" 421 logger.write_warning_to_html_report(message) 422 _GENERATED_PROFILE.name_field = BadStates.NO_RESPONSE.name 423 _GENERATED_PROFILE.name_violation = "invalid" 424 return 425 426 logger.write_result_to_html_report( 427 f"Found Identity Number: {_GENERATED_PROFILE.id} (0x{_GENERATED_PROFILE.id:06x}) " 428 f"for {_GENERATED_PROFILE.manufacturer_name} 6T at address {_GENERATED_PROFILE.address}" 429 ) 430 431 def test_comparison(self) -> None: 432 """ 433 | Description | Compare identity numbers | 434 | :------------------- | :--------------------------------------------------------------- | 435 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 436 | Instructions | 1. Confirm identity numbers are unique for each battery | 437 | Pass / Fail Criteria | Pass if identity numbers are unique for separate batteries | 438 | Estimated Duration | 1 second | 439 """ 440 failed_test = False 441 if not hasattr(_GENERATED_PROFILE, "id") or not hasattr(_COMPARISON_PROFILE, "id"): 442 logger.write_result_to_html_report("Nothing to compare. The profile test may have failed or been skipped") 443 pytest.skip("Nothing to compare. The profile test may have failed or been skipped") 444 445 logger.write_result_to_html_report( 446 f"Generated ID: {_GENERATED_PROFILE.id}, " f"Comparison ID: {_COMPARISON_PROFILE.id}" 447 ) 448 if _GENERATED_PROFILE.id == _COMPARISON_PROFILE.id: 449 logger.write_warning_to_html_report("Generated IDs are not unique values between batteries.") 450 else: 451 logger.write_result_to_html_report("Generated IDs are unique values") 452 453 if not hasattr(_GENERATED_PROFILE, "name_violation") or not hasattr(_COMPARISON_PROFILE, "name_violation"): 454 logger.write_warning_to_html_report("Missing specification information") 455 else: 456 comparison = cmp(_GENERATED_PROFILE.name_violation, "==", _COMPARISON_PROFILE.name_violation) 457 if _GENERATED_PROFILE.name_violation != _COMPARISON_PROFILE.name_violation: 458 logger.write_failure_to_html_report(f"Specification behavior mismatched: {comparison}") 459 failed_test = True 460 else: 461 logger.write_result_to_html_report(f"Specification behavior matched: {comparison}") 462 463 if failed_test: 464 message = "Comparisons did not meet expectations" 465 logger.write_failure_to_html_report(message) 466 pytest.fail(message)
Check Unique ID (Identity Number) field in Address Claimed message
332 @pytest.mark.profile 333 def test_profile(self) -> None: 334 """ 335 | Description | Confirm information in NAME (address claimed) matches \ 336 expected value. | 337 | :------------------- | :--------------------------------------------------------------- | 338 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 339 | Instructions | 1. Request Address Claimed data </br>\ 340 2. Log returned values </br>\ 341 3. Validate if values meet spec requirements | 342 | Estimated Duration | 21 seconds | 343 """ 344 345 name_values = {} 346 347 name_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 348 with CANBus(BATTERY_CHANNEL) as bus: 349 if name_frame := bus.process_call(name_request): 350 name_values["identity_number"] = int(name_frame.data[0]) 351 352 if name_values["identity_number"] != _GENERATED_PROFILE.id: 353 message = f"Identity Number {name_values} does not match Profile ID: {_GENERATED_PROFILE.id}" 354 logger.write_warning_to_html_report(message) 355 356 name_values["manufacturer_code"] = name_frame.data[1] 357 name_values["ecu_instance"] = name_frame.data[2] 358 name_values["function_instance"] = name_frame.data[3] 359 name_values["function"] = name_frame.data[4] 360 name_values["reserved"] = name_frame.data[5] 361 name_values["vehicle_system"] = name_frame.data[6] 362 name_values["vehicle_system_instance"] = name_frame.data[7] 363 name_values["industry_group"] = name_frame.data[8] 364 name_values["arbitrary_address_capable"] = name_frame.data[9] 365 366 for spn, elem in zip(PGN["Address Claimed"].data_field, name_frame.data): 367 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 368 logger.write_info_to_report(f"{spn.name}: {elem} {description}") 369 370 if spn.name in ("Manufacturer Code", "Reserved"): 371 continue 372 373 high_range = 0 374 if spn.name == "Identity Number": 375 high_range = 2097151 376 377 if spn.name == "ECU Instance": 378 high_range = 7 379 380 if spn.name == "Function Instance": 381 high_range = 31 382 383 if spn.name == "Function": 384 high_range = 254 385 386 if spn.name == "Vehicle System Instance": 387 high_range = 15 388 389 if spn.name == "Industry Group": 390 high_range = 7 391 392 if spn.name == "Arbitrary Address Capable": 393 high_range = 1 394 395 if spn.name == "Vehicle System": 396 if ( 397 name_values["vehicle_system"] != 127 398 and name_values["vehicle_system"] != 0 399 and name_values["vehicle_system"] != name_values["vehicle_system_instance"] 400 ): 401 logger.write_warning_to_html_report( 402 f"Vehicle System {elem} is not 127, 0, or the same value as " f"Vehicle System Instance" 403 ) 404 _GENERATED_PROFILE.name_violation = "invalid" 405 else: 406 _GENERATED_PROFILE.name_violation = "valid" 407 408 else: 409 if not 0 <= elem <= high_range: 410 logger.write_warning_to_html_report( 411 f"{spn.name}: {cmp(elem, '>=', 0)} and " f"{cmp(elem, '<=', high_range)}" 412 ) 413 _GENERATED_PROFILE.name_violation = "invalid" 414 else: 415 _GENERATED_PROFILE.name_violation = "valid" 416 417 _GENERATED_PROFILE.name_properties = name_values 418 419 else: 420 message = f"No response to PGN {PGN['Address Claimed', [32]].id} (Address Claimed)" 421 logger.write_warning_to_html_report(message) 422 _GENERATED_PROFILE.name_field = BadStates.NO_RESPONSE.name 423 _GENERATED_PROFILE.name_violation = "invalid" 424 return 425 426 logger.write_result_to_html_report( 427 f"Found Identity Number: {_GENERATED_PROFILE.id} (0x{_GENERATED_PROFILE.id:06x}) " 428 f"for {_GENERATED_PROFILE.manufacturer_name} 6T at address {_GENERATED_PROFILE.address}" 429 )
| Description | Confirm information in NAME (address claimed) matches expected value. |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#396 |
| Instructions | 1. Request Address Claimed data 2. Log returned values 3. Validate if values meet spec requirements |
| Estimated Duration | 21 seconds |
431 def test_comparison(self) -> None: 432 """ 433 | Description | Compare identity numbers | 434 | :------------------- | :--------------------------------------------------------------- | 435 | GitHub Issue | turnaroundfactor/BMS-HW-Test#396 | 436 | Instructions | 1. Confirm identity numbers are unique for each battery | 437 | Pass / Fail Criteria | Pass if identity numbers are unique for separate batteries | 438 | Estimated Duration | 1 second | 439 """ 440 failed_test = False 441 if not hasattr(_GENERATED_PROFILE, "id") or not hasattr(_COMPARISON_PROFILE, "id"): 442 logger.write_result_to_html_report("Nothing to compare. The profile test may have failed or been skipped") 443 pytest.skip("Nothing to compare. The profile test may have failed or been skipped") 444 445 logger.write_result_to_html_report( 446 f"Generated ID: {_GENERATED_PROFILE.id}, " f"Comparison ID: {_COMPARISON_PROFILE.id}" 447 ) 448 if _GENERATED_PROFILE.id == _COMPARISON_PROFILE.id: 449 logger.write_warning_to_html_report("Generated IDs are not unique values between batteries.") 450 else: 451 logger.write_result_to_html_report("Generated IDs are unique values") 452 453 if not hasattr(_GENERATED_PROFILE, "name_violation") or not hasattr(_COMPARISON_PROFILE, "name_violation"): 454 logger.write_warning_to_html_report("Missing specification information") 455 else: 456 comparison = cmp(_GENERATED_PROFILE.name_violation, "==", _COMPARISON_PROFILE.name_violation) 457 if _GENERATED_PROFILE.name_violation != _COMPARISON_PROFILE.name_violation: 458 logger.write_failure_to_html_report(f"Specification behavior mismatched: {comparison}") 459 failed_test = True 460 else: 461 logger.write_result_to_html_report(f"Specification behavior matched: {comparison}") 462 463 if failed_test: 464 message = "Comparisons did not meet expectations" 465 logger.write_failure_to_html_report(message) 466 pytest.fail(message)
| Description | Compare identity numbers |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#396 |
| Instructions | 1. Confirm identity numbers are unique for each battery |
| Pass / Fail Criteria | Pass if identity numbers are unique for separate batteries |
| Estimated Duration | 1 second |
469class TestSoftwareIdentificationInformation: 470 """Retrieve & Validate Software Identification Information""" 471 472 @pytest.mark.profile 473 def test_software_identification_information(self) -> None: 474 """ 475 | Description | Retrieve Software Identification | 476 | :------------------- | :--------------------------------------------------------------- | 477 | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 478 | Instructions | 1. Request Software Identification </br>\ 479 2. Validate Software Identification </br>\ 480 3. Log Response | 481 | Estimated Duration | 21 seconds | 482 """ 483 484 soft_request = CANFrame(pgn=PGN["RQST"], data=[0xFEDA]) 485 soft_data = [] 486 with CANBus(BATTERY_CHANNEL) as bus: 487 if soft_frame := bus.process_call(soft_request): 488 # Complete RTS 489 if len(soft_frame.data) < 3: 490 message = ( 491 f"Unexpected byte response from PGN " 492 f"{PGN['Software Identification', [32]].id} (Software Identification)" 493 ) 494 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 495 logger.write_warning_to_html_report(message) 496 _GENERATED_PROFILE.software_identification_specification = "invalid" 497 return 498 expected_bytes = int(soft_frame.data[1]) 499 expected_packets = 0 500 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.BRENTRONICS: 501 expected_packets = int(soft_frame.data[2] + 1) 502 else: 503 expected_packets = int(soft_frame.data[2]) 504 505 rts_pgn_id = int(soft_frame.data[-1]) 506 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 507 cts_request.data[1] = expected_packets 508 cts_request.data[-1] = rts_pgn_id 509 510 # Send CTS 511 bus.send_message(cts_request.message()) 512 513 # Read & Store data from packets 514 for i in range(expected_packets): 515 data_frame = bus.read_frame() 516 if data_frame.pgn.id != 0xEB00: 517 message = f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id}" 518 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 519 _GENERATED_PROFILE.software_identification_specification = "invalid" 520 logger.write_warning_to_html_report(message) 521 return 522 soft_data.append(hex(int(data_frame.data[1]))) 523 524 if len(soft_data) != expected_packets: 525 message = f"Expected {expected_packets} packets, got {len(soft_data)}" 526 logger.write_warning_to_html_report(message) 527 _GENERATED_PROFILE.software_identification_specification = "invalid" 528 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 529 return 530 531 # Send acknowledgement frame 532 end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 533 end_acknowledge_frame.data[1] = expected_bytes 534 end_acknowledge_frame.data[2] = expected_packets 535 end_acknowledge_frame.data[-1] = rts_pgn_id 536 537 bus.send_message(end_acknowledge_frame.message()) 538 else: 539 message = f"No response for PGN {PGN['Software Identification', [32]].id} (Software Identification)" 540 logger.write_warning_to_html_report(message) 541 _GENERATED_PROFILE.software_identification_information = BadStates.NO_RESPONSE.name 542 _GENERATED_PROFILE.software_identification_specification = "invalid" 543 return 544 545 hex_string = "0x" 546 add_to_data = False 547 finish_adding = False 548 549 # Find Software Identification Number 550 for element in soft_data: 551 n = len(element) - 1 552 while n >= 1: 553 if add_to_data: 554 if element[n - 1 : n + 1] == "2a": 555 finish_adding = True 556 break 557 if element[n - 1 : n + 1] != "0x": 558 hex_string += element[n - 1 : n + 1] 559 n -= 2 560 else: 561 n -= 2 562 else: 563 if element[n - 3 : n + 1] == "2a31" or element[n - 3 : n + 1] == "2a01": 564 add_to_data = True 565 n -= 4 566 else: 567 n -= 4 568 if finish_adding: 569 break 570 571 if len(hex_string) <= 2: 572 logger.write_warning_to_html_report( 573 f"Software Identification: {hex_string} was not found in expected format" 574 ) 575 _GENERATED_PROFILE.software_identification_information = hex_string 576 _GENERATED_PROFILE.software_identification_specification = "invalid" 577 return 578 579 software_identification = spn_types.ascii_map(int(hex_string, 16)) 580 581 if re.search(r"[0-9]{2}\.[0-9]{2}\.[a-z]{2}\.[a-z]{2,}", software_identification) is None: 582 message = f"Software Identification: {software_identification} was not in expected format: MM.II.mm.aa.ee" 583 logger.write_warning_to_html_report(message) 584 _GENERATED_PROFILE.software_identification_specification = "invalid" 585 else: 586 _GENERATED_PROFILE.software_identification_specification = "valid" 587 588 _GENERATED_PROFILE.software_identification = software_identification 589 590 logger.write_result_to_html_report(f"Software Identification: {software_identification}") 591 592 def test_comparison(self) -> None: 593 """ 594 | Description | Compare Software Identification values | 595 | :------------------- | :--------------------------------------------------------------- | 596 | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 597 | Instructions | 1. Confirm software versions are the same for each profile | 598 | Pass / Fail Criteria | Pass if identity numbers match for each profile | 599 | Estimated Duration | 1 second | 600 """ 601 failed_test = False 602 if not hasattr(_GENERATED_PROFILE, "software_identification") or not hasattr( 603 _COMPARISON_PROFILE, "software_identification" 604 ): 605 message = "Nothing to compare. The profile test may have failed or been skipped" 606 logger.write_result_to_html_report(message) 607 pytest.skip(message) 608 609 compare = cmp(_GENERATED_PROFILE.software_identification, "==", _COMPARISON_PROFILE.software_identification) 610 if _GENERATED_PROFILE.software_identification != _COMPARISON_PROFILE.software_identification: 611 logger.write_failure_to_html_report(f"Software Identification: {compare}") 612 failed_test = True 613 else: 614 logger.write_result_to_html_report(f"Software Identification: {compare}") 615 616 if not hasattr(_GENERATED_PROFILE, "software_identification_specification") or not hasattr( 617 _COMPARISON_PROFILE, "software_identification_specification" 618 ): 619 logger.write_warning_to_html_report("Missing specification information") 620 else: 621 comparison = cmp( 622 _GENERATED_PROFILE.software_identification_specification, 623 "==", 624 _COMPARISON_PROFILE.software_identification_specification, 625 ) 626 if ( 627 _GENERATED_PROFILE.software_identification_specification 628 != _COMPARISON_PROFILE.software_identification_specification 629 ): 630 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 631 failed_test = True 632 else: 633 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 634 635 if failed_test: 636 message = "Comparisons did not match in profiles" 637 logger.write_failure_to_html_report(message) 638 pytest.fail(message)
Retrieve & Validate Software Identification Information
472 @pytest.mark.profile 473 def test_software_identification_information(self) -> None: 474 """ 475 | Description | Retrieve Software Identification | 476 | :------------------- | :--------------------------------------------------------------- | 477 | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 478 | Instructions | 1. Request Software Identification </br>\ 479 2. Validate Software Identification </br>\ 480 3. Log Response | 481 | Estimated Duration | 21 seconds | 482 """ 483 484 soft_request = CANFrame(pgn=PGN["RQST"], data=[0xFEDA]) 485 soft_data = [] 486 with CANBus(BATTERY_CHANNEL) as bus: 487 if soft_frame := bus.process_call(soft_request): 488 # Complete RTS 489 if len(soft_frame.data) < 3: 490 message = ( 491 f"Unexpected byte response from PGN " 492 f"{PGN['Software Identification', [32]].id} (Software Identification)" 493 ) 494 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 495 logger.write_warning_to_html_report(message) 496 _GENERATED_PROFILE.software_identification_specification = "invalid" 497 return 498 expected_bytes = int(soft_frame.data[1]) 499 expected_packets = 0 500 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.BRENTRONICS: 501 expected_packets = int(soft_frame.data[2] + 1) 502 else: 503 expected_packets = int(soft_frame.data[2]) 504 505 rts_pgn_id = int(soft_frame.data[-1]) 506 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 507 cts_request.data[1] = expected_packets 508 cts_request.data[-1] = rts_pgn_id 509 510 # Send CTS 511 bus.send_message(cts_request.message()) 512 513 # Read & Store data from packets 514 for i in range(expected_packets): 515 data_frame = bus.read_frame() 516 if data_frame.pgn.id != 0xEB00: 517 message = f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id}" 518 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 519 _GENERATED_PROFILE.software_identification_specification = "invalid" 520 logger.write_warning_to_html_report(message) 521 return 522 soft_data.append(hex(int(data_frame.data[1]))) 523 524 if len(soft_data) != expected_packets: 525 message = f"Expected {expected_packets} packets, got {len(soft_data)}" 526 logger.write_warning_to_html_report(message) 527 _GENERATED_PROFILE.software_identification_specification = "invalid" 528 _GENERATED_PROFILE.software_identification_information = BadStates.WRONG_PACKETS.name 529 return 530 531 # Send acknowledgement frame 532 end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 533 end_acknowledge_frame.data[1] = expected_bytes 534 end_acknowledge_frame.data[2] = expected_packets 535 end_acknowledge_frame.data[-1] = rts_pgn_id 536 537 bus.send_message(end_acknowledge_frame.message()) 538 else: 539 message = f"No response for PGN {PGN['Software Identification', [32]].id} (Software Identification)" 540 logger.write_warning_to_html_report(message) 541 _GENERATED_PROFILE.software_identification_information = BadStates.NO_RESPONSE.name 542 _GENERATED_PROFILE.software_identification_specification = "invalid" 543 return 544 545 hex_string = "0x" 546 add_to_data = False 547 finish_adding = False 548 549 # Find Software Identification Number 550 for element in soft_data: 551 n = len(element) - 1 552 while n >= 1: 553 if add_to_data: 554 if element[n - 1 : n + 1] == "2a": 555 finish_adding = True 556 break 557 if element[n - 1 : n + 1] != "0x": 558 hex_string += element[n - 1 : n + 1] 559 n -= 2 560 else: 561 n -= 2 562 else: 563 if element[n - 3 : n + 1] == "2a31" or element[n - 3 : n + 1] == "2a01": 564 add_to_data = True 565 n -= 4 566 else: 567 n -= 4 568 if finish_adding: 569 break 570 571 if len(hex_string) <= 2: 572 logger.write_warning_to_html_report( 573 f"Software Identification: {hex_string} was not found in expected format" 574 ) 575 _GENERATED_PROFILE.software_identification_information = hex_string 576 _GENERATED_PROFILE.software_identification_specification = "invalid" 577 return 578 579 software_identification = spn_types.ascii_map(int(hex_string, 16)) 580 581 if re.search(r"[0-9]{2}\.[0-9]{2}\.[a-z]{2}\.[a-z]{2,}", software_identification) is None: 582 message = f"Software Identification: {software_identification} was not in expected format: MM.II.mm.aa.ee" 583 logger.write_warning_to_html_report(message) 584 _GENERATED_PROFILE.software_identification_specification = "invalid" 585 else: 586 _GENERATED_PROFILE.software_identification_specification = "valid" 587 588 _GENERATED_PROFILE.software_identification = software_identification 589 590 logger.write_result_to_html_report(f"Software Identification: {software_identification}")
| Description | Retrieve Software Identification |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#394 |
| Instructions | 1. Request Software Identification 2. Validate Software Identification 3. Log Response |
| Estimated Duration | 21 seconds |
592 def test_comparison(self) -> None: 593 """ 594 | Description | Compare Software Identification values | 595 | :------------------- | :--------------------------------------------------------------- | 596 | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 597 | Instructions | 1. Confirm software versions are the same for each profile | 598 | Pass / Fail Criteria | Pass if identity numbers match for each profile | 599 | Estimated Duration | 1 second | 600 """ 601 failed_test = False 602 if not hasattr(_GENERATED_PROFILE, "software_identification") or not hasattr( 603 _COMPARISON_PROFILE, "software_identification" 604 ): 605 message = "Nothing to compare. The profile test may have failed or been skipped" 606 logger.write_result_to_html_report(message) 607 pytest.skip(message) 608 609 compare = cmp(_GENERATED_PROFILE.software_identification, "==", _COMPARISON_PROFILE.software_identification) 610 if _GENERATED_PROFILE.software_identification != _COMPARISON_PROFILE.software_identification: 611 logger.write_failure_to_html_report(f"Software Identification: {compare}") 612 failed_test = True 613 else: 614 logger.write_result_to_html_report(f"Software Identification: {compare}") 615 616 if not hasattr(_GENERATED_PROFILE, "software_identification_specification") or not hasattr( 617 _COMPARISON_PROFILE, "software_identification_specification" 618 ): 619 logger.write_warning_to_html_report("Missing specification information") 620 else: 621 comparison = cmp( 622 _GENERATED_PROFILE.software_identification_specification, 623 "==", 624 _COMPARISON_PROFILE.software_identification_specification, 625 ) 626 if ( 627 _GENERATED_PROFILE.software_identification_specification 628 != _COMPARISON_PROFILE.software_identification_specification 629 ): 630 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 631 failed_test = True 632 else: 633 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 634 635 if failed_test: 636 message = "Comparisons did not match in profiles" 637 logger.write_failure_to_html_report(message) 638 pytest.fail(message)
| Description | Compare Software Identification values |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#394 |
| Instructions | 1. Confirm software versions are the same for each profile |
| Pass / Fail Criteria | Pass if identity numbers match for each profile |
| Estimated Duration | 1 second |
641class TestBatteryRegulationInformation: 642 """Retrieve and validate battery regulation information""" 643 644 @pytest.mark.profile 645 def test_profile(self) -> None: 646 """ 647 | Description | Obtain Battery Regulation Information 1 & 2 | 648 | :------------------- | :--------------------------------------------------------------- | 649 | GitHub Issue | turnaroundfactor/BMS-HW-Test#392 | 650 | Instructions | 1. Request Battery Regulation Information 1 </br>\ 651 2. Validate Information </br>\ 652 3. Log Response | 653 | Estimated Duration | 22 seconds | 654 """ 655 656 battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03]) 657 battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04]) 658 659 # Obtain Battery Regulation One Information 660 with CANBus(BATTERY_CHANNEL) as bus: 661 if battery_frame := bus.process_call(battery_regulation_one_request): 662 logger.write_result_to_html_report( 663 "<span style='font-weight: bold'>Battery Regulation Information 1 </span>" 664 ) 665 if battery_frame.pgn.id != PGN["PropB_03", [32]].id: 666 message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}" 667 logger.write_warning_to_html_report(message) 668 _GENERATED_PROFILE.battery_regulation_one = BadStates.WRONG_PGN.name 669 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 670 else: 671 _GENERATED_PROFILE.battery_regulation_one = { 672 "battery_voltage": battery_frame.data[0], 673 "open_circuit_voltage": battery_frame.data[1], 674 "battery_current": battery_frame.data[2], 675 "maximum_charge_current": battery_frame.data[3], 676 } 677 678 for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data): 679 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 680 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 681 682 high_range = 3212.75 683 low_range = 0.0 684 if spn.name == "Battery Current": 685 high_range = 1600.00 686 low_range = -1600.00 687 688 if not low_range <= elem <= high_range: 689 logger.write_warning_to_html_report( 690 f"{spn.name}: {cmp(elem, '>=', low_range)} and " f"{cmp(elem, '<=', high_range)}" 691 ) 692 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 693 694 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one_specification"): 695 _GENERATED_PROFILE.battery_regulation_one_specification = "valid" 696 697 else: 698 message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)" 699 logger.write_warning_to_html_report(message) 700 _GENERATED_PROFILE.battery_regulation_one = BadStates.NO_RESPONSE.name 701 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 702 703 # Obtain Battery Regulation Two Information 704 with CANBus(BATTERY_CHANNEL) as bus: 705 if battery_frame_two := bus.process_call(battery_regulation_two_request): 706 logger.write_result_to_html_report( 707 "<span style='font-weight: bold'>Battery Regulation Information 2</span>" 708 ) 709 710 if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id: 711 message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}" 712 logger.write_warning_to_html_report(message) 713 _GENERATED_PROFILE.battery_regulation_two = BadStates.NO_RESPONSE.name 714 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 715 else: 716 _GENERATED_PROFILE.battery_regulation_two = { 717 "contractor_state": battery_frame_two.data[0], 718 "charge_capability_state": battery_frame_two.data[1], 719 "bus_voltage_request": battery_frame_two.data[3], 720 "transportability_soc": battery_frame_two.data[4], 721 } 722 723 for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data): 724 725 if spn.name == "Reserved": 726 continue 727 728 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 729 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 730 731 high_range = 3 732 if spn.name == "Bus Voltage Request": 733 high_range = 3212.75 734 735 if spn.name == "Transportability SOC": 736 high_range = 100 737 738 if not 0 <= elem <= high_range: 739 logger.write_warning_to_html_report( 740 f"{spn.name}: {cmp(elem, '>=', 0)} and {cmp(elem, '<=', high_range)}" 741 ) 742 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 743 744 if not hasattr(_GENERATED_PROFILE, "battery_regulation_two_specification"): 745 _GENERATED_PROFILE.battery_regulation_two_specification = "valid" 746 747 else: 748 message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)" 749 logger.write_warning_to_html_report(message) 750 _GENERATED_PROFILE.battery_regulation_two = BadStates.NO_RESPONSE.name 751 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 752 753 # def test_comparison(self) -> None: 754 # # TODO: FIX 755 # """ 756 # | Description | Compare Battery Regulation values | 757 # | :------------------- | :--------------------------------------------------------------- | 758 # | GitHub Issue | turnaroundfactor/BMS-HW-Test#394 | 759 # | Instructions | 1. Confirm Battery Regulation One profiles match </br>\ 760 # 2. Confirm Battery Regulation Two profiles match | 761 # | Pass / Fail Criteria | Pass if Battery Regulation values match for each profile | 762 # | Estimated Duration | 1 second | 763 # """ 764 # 765 # def loop_profiles(generated, comparison) -> list[str]: 766 # fail_list = [] 767 # for key, value in generated.items(): 768 # key_text = key.replace("_", " ").title() 769 # message = cmp(value, "==", comparison[key]) 770 # if comparison[key] != value: 771 # logger.write_warning_to_html_report(f"{key_text}: {message}") 772 # fail_list.append(key_text) 773 # else: 774 # logger.write_result_to_html_report(f"{key_text}: {message}") 775 # 776 # return fail_list 777 # 778 # test_failed = False 779 # reg_one_list: list[str] = [] 780 # reg_two_list: list[str] = [] 781 # if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") or not hasattr( 782 # _COMPARISON_PROFILE, "battery_regulation_one" 783 # ): 784 # message = "Nothing to compare. The profile test may have failed or been skipped" 785 # logger.write_result_to_html_report(message) 786 # pytest.skip(message) 787 # 788 # if not hasattr(_GENERATED_PROFILE, "battery_regulation_two") or not hasattr( 789 # _COMPARISON_PROFILE, "battery_regulation_two" 790 # ): 791 # message = "Nothing to compare. The profile test may have failed or been skipped" 792 # logger.write_result_to_html_report(message) 793 # pytest.skip(message) 794 # 795 # logger.write_result_to_html_report( 796 # "<span style='font-weight: bold'>Battery Regulation One Comparisons</span>" 797 # ) 798 # if isinstance(_GENERATED_PROFILE.battery_regulation_one, str) or isinstance( 799 # _COMPARISON_PROFILE.battery_regulation_one, str 800 # ): 801 # if not isinstance( 802 # _GENERATED_PROFILE.battery_regulation_one, type(_COMPARISON_PROFILE.battery_regulation_one) 803 # ): 804 # message = "Unable to compare profiles, Battery Regulation One types did not match" 805 # logger.write_warning_to_html_report(message) 806 # pytest.fail(message) 807 # 808 # comparison_text = cmp( 809 # _GENERATED_PROFILE.battery_regulation_one, "==", _COMPARISON_PROFILE.battery_regulation_one 810 # ) 811 # 812 # if _GENERATED_PROFILE.battery_regulation_one != _COMPARISON_PROFILE.battery_regulation_one: 813 # message = f"Battery Regulation One did not match: {comparison_text}" 814 # logger.write_warning_to_html_report(message) 815 # test_failed = True 816 # else: 817 # reg_one_list = loop_profiles( 818 # _GENERATED_PROFILE.battery_regulation_one, _COMPARISON_PROFILE.battery_regulation_one 819 # ) 820 # 821 # logger.write_result_to_html_report( 822 # "<span style='font-weight: bold'>Battery Regulation Two Comparisons</span>" 823 # ) 824 # if isinstance(_GENERATED_PROFILE.battery_regulation_two, str) or isinstance( 825 # _COMPARISON_PROFILE.battery_regulation_two, str 826 # ): 827 # if not isinstance( 828 # _GENERATED_PROFILE.battery_regulation_two, type(_COMPARISON_PROFILE.battery_regulation_two) 829 # ): 830 # message = "Unable to compare profiles, Battery Regulation One types did not match" 831 # logger.write_warning_to_html_report(message) 832 # pytest.fail(message) 833 # 834 # comparison_text = cmp( 835 # _GENERATED_PROFILE.battery_regulation_two, "==", _COMPARISON_PROFILE.battery_regulation_two 836 # ) 837 # 838 # if _GENERATED_PROFILE.battery_regulation_two != _COMPARISON_PROFILE.battery_regulation_two: 839 # message = f"Battery Regulation Two did not match: {comparison_text}" 840 # logger.write_warning_to_html_report(message) 841 # test_failed = True 842 # else: 843 # reg_two_list = loop_profiles( 844 # _GENERATED_PROFILE.battery_regulation_two, _COMPARISON_PROFILE.battery_regulation_two 845 # ) 846 # 847 # if test_failed: 848 # message = "Battery Regulation values did not match" 849 # logger.write_failure_to_html_report(message) 850 # pytest.fail(message) 851 # 852 # fail_categories = reg_one_list + reg_two_list 853 # 854 # if len(fail_categories) > 0: 855 # categories = "data values" if len(fail_categories) > 1 else "data value" 856 # message = f"{len(fail_categories)} {categories} failed profile comparisons: {', '.join(fail_categories)}" 857 # logger.write_failure_to_html_report(message) 858 # pytest.fail(message) 859 # else: 860 # logger.write_result_to_html_report("Battery Regulation 1 and 2 profiles match")
Retrieve and validate battery regulation information
644 @pytest.mark.profile 645 def test_profile(self) -> None: 646 """ 647 | Description | Obtain Battery Regulation Information 1 & 2 | 648 | :------------------- | :--------------------------------------------------------------- | 649 | GitHub Issue | turnaroundfactor/BMS-HW-Test#392 | 650 | Instructions | 1. Request Battery Regulation Information 1 </br>\ 651 2. Validate Information </br>\ 652 3. Log Response | 653 | Estimated Duration | 22 seconds | 654 """ 655 656 battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03]) 657 battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04]) 658 659 # Obtain Battery Regulation One Information 660 with CANBus(BATTERY_CHANNEL) as bus: 661 if battery_frame := bus.process_call(battery_regulation_one_request): 662 logger.write_result_to_html_report( 663 "<span style='font-weight: bold'>Battery Regulation Information 1 </span>" 664 ) 665 if battery_frame.pgn.id != PGN["PropB_03", [32]].id: 666 message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}" 667 logger.write_warning_to_html_report(message) 668 _GENERATED_PROFILE.battery_regulation_one = BadStates.WRONG_PGN.name 669 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 670 else: 671 _GENERATED_PROFILE.battery_regulation_one = { 672 "battery_voltage": battery_frame.data[0], 673 "open_circuit_voltage": battery_frame.data[1], 674 "battery_current": battery_frame.data[2], 675 "maximum_charge_current": battery_frame.data[3], 676 } 677 678 for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data): 679 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 680 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 681 682 high_range = 3212.75 683 low_range = 0.0 684 if spn.name == "Battery Current": 685 high_range = 1600.00 686 low_range = -1600.00 687 688 if not low_range <= elem <= high_range: 689 logger.write_warning_to_html_report( 690 f"{spn.name}: {cmp(elem, '>=', low_range)} and " f"{cmp(elem, '<=', high_range)}" 691 ) 692 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 693 694 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one_specification"): 695 _GENERATED_PROFILE.battery_regulation_one_specification = "valid" 696 697 else: 698 message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)" 699 logger.write_warning_to_html_report(message) 700 _GENERATED_PROFILE.battery_regulation_one = BadStates.NO_RESPONSE.name 701 _GENERATED_PROFILE.battery_regulation_one_specification = "invalid" 702 703 # Obtain Battery Regulation Two Information 704 with CANBus(BATTERY_CHANNEL) as bus: 705 if battery_frame_two := bus.process_call(battery_regulation_two_request): 706 logger.write_result_to_html_report( 707 "<span style='font-weight: bold'>Battery Regulation Information 2</span>" 708 ) 709 710 if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id: 711 message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}" 712 logger.write_warning_to_html_report(message) 713 _GENERATED_PROFILE.battery_regulation_two = BadStates.NO_RESPONSE.name 714 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 715 else: 716 _GENERATED_PROFILE.battery_regulation_two = { 717 "contractor_state": battery_frame_two.data[0], 718 "charge_capability_state": battery_frame_two.data[1], 719 "bus_voltage_request": battery_frame_two.data[3], 720 "transportability_soc": battery_frame_two.data[4], 721 } 722 723 for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data): 724 725 if spn.name == "Reserved": 726 continue 727 728 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 729 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 730 731 high_range = 3 732 if spn.name == "Bus Voltage Request": 733 high_range = 3212.75 734 735 if spn.name == "Transportability SOC": 736 high_range = 100 737 738 if not 0 <= elem <= high_range: 739 logger.write_warning_to_html_report( 740 f"{spn.name}: {cmp(elem, '>=', 0)} and {cmp(elem, '<=', high_range)}" 741 ) 742 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid" 743 744 if not hasattr(_GENERATED_PROFILE, "battery_regulation_two_specification"): 745 _GENERATED_PROFILE.battery_regulation_two_specification = "valid" 746 747 else: 748 message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)" 749 logger.write_warning_to_html_report(message) 750 _GENERATED_PROFILE.battery_regulation_two = BadStates.NO_RESPONSE.name 751 _GENERATED_PROFILE.battery_regulation_two_specification = "invalid"
| Description | Obtain Battery Regulation Information 1 & 2 |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#392 |
| Instructions | 1. Request Battery Regulation Information 1 2. Validate Information 3. Log Response |
| Estimated Duration | 22 seconds |
863class TestPowerSupply: 864 """Retrieve and validate battery regulation information""" 865 866 @pytest.mark.profile 867 def test_charge_power_supply(self) -> None: 868 """ 869 | Description | Check Battery Information after Charge | 870 | :------------------- | :--------------------------------------------------------------- | 871 | GitHub Issue | turnaroundfactor/BMS-HW-Test#597 | 872 | Instructions | 1. Request Battery State of Charge </br>\ 873 2. Charge battery at 0.1A for 1 minute </br>\ 874 3. Check & save Battery Regulation 1 & 2 information </br>\ 875 4. Log Response | 876 | Estimated Duration | 2 minutes | 877 """ 878 879 battery_calculation_one = CANFrame(pgn=PGN["RQST"], data=[PGN["PropB_02", [32]].id]) 880 881 with CANBus(BATTERY_CHANNEL) as bus: 882 if battery_calculation_response := bus.process_call(battery_calculation_one): 883 if battery_calculation_response.pgn.id != PGN["PropB_02", [32]].id: 884 message = ( 885 f"Expected PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name}) " 886 f" but received PGN {battery_calculation_response.pgn.id}" 887 ) 888 logger.write_warning_to_html_report(message) 889 _GENERATED_PROFILE.test_power_supply = BadStates.WRONG_PGN.name 890 pytest.skip(message) 891 892 for spn, elem in zip(PGN["PropB_02"].data_field, battery_calculation_response.data): 893 low_range = 0 894 high_range = 95 895 if spn.name == "Battery State of Charge": 896 comparison = f"{cmp(elem, '>=', low_range, '%')} " f"and {cmp(elem, '<=', high_range, '%' )}" 897 898 if not low_range <= elem <= high_range: 899 message = ( 900 f"Skipping Test: {spn.name} is {elem}%, " 901 f"which is not within expected range: {comparison}" 902 ) 903 logger.write_warning_to_html_report(message) 904 _GENERATED_PROFILE.test_power_supply = BadStates.SKIPPED_TEST.name 905 pytest.skip(message) 906 else: 907 message = f"{spn.name} is {elem}%" 908 logger.write_result_to_html_report(message) 909 break 910 else: 911 message = f"Did not receive response for PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name})" 912 logger.write_warning_to_html_report(message) 913 _GENERATED_PROFILE.test_power_supply = BadStates.NO_RESPONSE.name 914 pytest.skip(message) 915 916 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") and not hasattr( 917 _GENERATED_PROFILE, "battery_regulation_two" 918 ): 919 default_b_one = self.get_battery_regulation_one() 920 default_b_two = self.get_battery_regulation_two() 921 else: 922 default_b_one = _GENERATED_PROFILE.battery_regulation_one 923 default_b_two = _GENERATED_PROFILE.battery_regulation_two 924 925 if isinstance(default_b_one, str) or isinstance(default_b_two, str): 926 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 927 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 928 pytest.skip("Missing valid Battery Regulation One and Two info") 929 930 logger.write_result_to_html_report("Charging battery") 931 try: 932 cyber_charger = charger.Charger() 933 except RuntimeError: 934 message = "Could not communicate with power supply" 935 logger.write_warning_to_html_report(message) 936 pytest.fail(message) 937 938 try: 939 with cyber_charger(0.1, float(default_b_two.get("bus_voltage_request", 0.0))): 940 logger.write_info_to_report("Battery Regulation One Values from Charge") 941 updated_b_one = self.get_battery_regulation_one() 942 logger.write_info_to_report("Battery Regulation Two Values from Charge") 943 updated_b_two = self.get_battery_regulation_two() 944 945 if isinstance(updated_b_one, str): 946 if updated_b_one == "No Current": 947 _GENERATED_PROFILE.test_charge_power_supply = "Current not detected" 948 raise RuntimeError 949 950 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 951 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 952 pytest.skip("Missing valid Battery Regulation One and Two info") 953 954 _GENERATED_PROFILE.charge_b_one = updated_b_one 955 _GENERATED_PROFILE.charge_b_two = updated_b_two 956 957 except RuntimeError: 958 if cyber_charger.resource: 959 if hasattr(cyber_charger.resource, "close"): 960 cyber_charger.resource.close() 961 message = "Could not communicate with power supply or current not detected" 962 logger.write_warning_to_html_report(message) 963 964 if not hasattr(_GENERATED_PROFILE, "test_charge_power_supply"): 965 _GENERATED_PROFILE.test_charge_power_supply = "Could not communicate" 966 pytest.fail(message) 967 968 except AttributeError: 969 message = "Could not communicate with power supply" 970 logger.write_warning_to_html_report(message) 971 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 972 pytest.fail(message) 973 974 def test_charge_comparison(self): 975 """ 976 | Description | Compare Power Supply Charge values | 977 | :------------------- | :--------------------------------------------------------------- | 978 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 979 | Instructions | 1. Check if charge Battery Current is within 10% of initial value| 980 | Pass / Fail Criteria | Pass if charged value is within 10% | 981 | Estimated Duration | 1 second | 982 """ 983 984 if hasattr(_GENERATED_PROFILE, "test_power_supply") or not hasattr(_GENERATED_PROFILE, "charge_b_one"): 985 message = "Nothing to compare. Profile test may have failed or been skipped" 986 logger.write_result_to_html_report(message) 987 pytest.skip(message) 988 989 if hasattr(_COMPARISON_PROFILE, "test_power_supply") or not hasattr(_COMPARISON_PROFILE, "charge_b_one"): 990 message = "Nothing to compare. Profile test may have failed or been skipped" 991 logger.write_result_to_html_report(message) 992 pytest.skip(message) 993 994 if isinstance(_GENERATED_PROFILE.charge_b_one, str) or isinstance(_COMPARISON_PROFILE.charge_b_one, str): 995 if not isinstance(_GENERATED_PROFILE.charge_b_one, type(_COMPARISON_PROFILE.charge_b_one)): 996 message = "Unable to compare profiles, types of Battery Regulation Information 1 did not match" 997 logger.write_failure_to_html_report(message) 998 pytest.fail(message) 999 1000 comparison = cmp(_GENERATED_PROFILE.charge_b_one, "==", _COMPARISON_PROFILE.charge_b_one) 1001 1002 if _GENERATED_PROFILE.charge_b_one != _COMPARISON_PROFILE.charge_b_one: 1003 message = f"Battery Regulation Information 1 matched profiles: {comparison}" 1004 logger.write_failure_to_html_report(message) 1005 pytest.fail(message) 1006 1007 message = f"Battery Regulation Information 1 matched: {comparison}" 1008 logger.write_result_to_html_report(message) 1009 return 1010 1011 original_current = _GENERATED_PROFILE.charge_b_one.get("battery_current") 1012 comparison_current = _COMPARISON_PROFILE.charge_b_one.get("battery_current") 1013 1014 diff = abs(original_current - comparison_current) 1015 average = abs(original_current + comparison_current) / 2 1016 percentage = round((diff / average), 4) 1017 percent_closeness = 0.1 1018 1019 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 1020 1021 if not math.isclose(original_current, comparison_current, rel_tol=0.1): 1022 logger.write_warning_to_html_report( 1023 f"Battery Regulation Information 1 current percent difference: {comparison}" 1024 ) 1025 logger.write_failure_to_html_report("Current was not within expected range during charge") 1026 pytest.fail("Current was not within expected range during charge") 1027 1028 logger.write_result_to_html_report(f"Battery Regulation Information 1 current percent difference: {comparison}") 1029 logger.write_result_to_html_report("Current was within expected range during charge") 1030 1031 @pytest.mark.profile 1032 def test_discharge_power_supply(self) -> None: 1033 """ 1034 | Description | Check Battery Information after Discharge | 1035 | :------------------- | :--------------------------------------------------------------- | 1036 | GitHub Issue | turnaroundfactor/BMS-HW-Test#597 | 1037 | Instructions | 1. Request Battery State of Charge </br>\ 1038 2. Discharge battery at -0.1A for 1 minute </br>\ 1039 3. Check & save Battery Regulation 1 & 2 information </br>\ 1040 4. Log Response | 1041 | Estimated Duration | 2 minutes | 1042 """ 1043 1044 battery_calculation_one = CANFrame(pgn=PGN["RQST"], data=[PGN["PropB_02", [32]].id]) 1045 1046 with CANBus(BATTERY_CHANNEL) as bus: 1047 if battery_calculation_response := bus.process_call(battery_calculation_one): 1048 if battery_calculation_response.pgn.id != PGN["PropB_02", [32]].id: 1049 message = ( 1050 f"Expected PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name}) " 1051 f" but received PGN {battery_calculation_response.pgn.id}" 1052 ) 1053 logger.write_warning_to_html_report(message) 1054 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.WRONG_PGN.name 1055 return 1056 1057 for spn, elem in zip(PGN["PropB_02"].data_field, battery_calculation_response.data): 1058 if spn.name == "Battery State of Charge": 1059 low_range = 5 1060 high_range = 100 1061 comparison = f"{cmp(elem, '>=', low_range, '%')} and {cmp(elem, '<=', high_range, '%' )}" 1062 1063 if not low_range <= elem <= high_range: 1064 message = ( 1065 f"Skipping Test: {spn.name} is {elem}%, " 1066 f"which is not within expected range: {comparison}" 1067 ) 1068 logger.write_warning_to_html_report(message) 1069 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.SKIPPED_TEST.name 1070 1071 pytest.skip(message) 1072 else: 1073 message = f"{spn.name} is {elem}%" 1074 logger.write_result_to_html_report(message) 1075 break 1076 else: 1077 message = f"Did not receive response for PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name})" 1078 logger.write_warning_to_html_report(message) 1079 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.NO_RESPONSE.name 1080 pytest.skip(message) 1081 1082 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") and not hasattr( 1083 _GENERATED_PROFILE, "battery_regulation_two" 1084 ): 1085 default_b_one = self.get_battery_regulation_one() 1086 default_b_two = self.get_battery_regulation_two() 1087 else: 1088 default_b_one = _GENERATED_PROFILE.battery_regulation_one 1089 default_b_two = _GENERATED_PROFILE.battery_regulation_two 1090 1091 if isinstance(default_b_one, str) or isinstance(default_b_two, str): 1092 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.INVALID_RESPONSE.name 1093 1094 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 1095 pytest.skip("Missing valid Battery Regulation One and Two info") 1096 1097 logger.write_result_to_html_report("Discharging battery") 1098 try: 1099 cyber_charger = charger.Charger() 1100 except RuntimeError: 1101 message = "Could not communicate with power supply" 1102 logger.write_warning_to_html_report(message) 1103 pytest.fail(message) 1104 try: 1105 with cyber_charger(-0.1): 1106 logger.write_info_to_report("Battery Regulation One Values from Discharge") 1107 updated_b_one = self.get_battery_regulation_one() 1108 logger.write_info_to_report("Battery Regulation Two Values from Discharge") 1109 updated_b_two = self.get_battery_regulation_two() 1110 1111 if isinstance(updated_b_one, str): 1112 if updated_b_one == "No Current": 1113 _GENERATED_PROFILE.test_discharge_power_supply = "Current not detected" 1114 raise RuntimeError 1115 1116 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 1117 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 1118 pytest.skip("Missing valid Battery Regulation One and Two info") 1119 1120 _GENERATED_PROFILE.discharge_b_one = updated_b_one 1121 _GENERATED_PROFILE.discharge_b_two = updated_b_two 1122 1123 except RuntimeError: 1124 if cyber_charger.resource: 1125 if hasattr(cyber_charger.resource, "close"): 1126 cyber_charger.resource.close() 1127 message = "Could not communicate with power supply or current not detected" 1128 logger.write_warning_to_html_report(message) 1129 if not hasattr(_GENERATED_PROFILE, "test_charge_power_supply"): 1130 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 1131 pytest.fail(message) 1132 1133 except AttributeError: 1134 message = "Could not communicate with power supply" 1135 logger.write_warning_to_html_report(message) 1136 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 1137 pytest.fail(message) 1138 1139 def test_discharge_comparison(self): 1140 """ 1141 | Description | Compare Power Supply Discharge values | 1142 | :------------------- | :--------------------------------------------------------------- | 1143 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1144 | Instructions | 1. Check if discharge Battery Current is within 10% of initial value| 1145 | Pass / Fail Criteria | Pass if discharged value is within 10% | 1146 | Estimated Duration | 1 second | 1147 """ 1148 1149 if hasattr(_GENERATED_PROFILE, "test_discharge_power_supply") or not hasattr( 1150 _GENERATED_PROFILE, "discharge_b_one" 1151 ): 1152 message = "Nothing to compare. The profile test may have failed or been skipped" 1153 logger.write_result_to_html_report(message) 1154 pytest.skip(message) 1155 1156 if hasattr(_COMPARISON_PROFILE, "test_discharge_power_supply") or not hasattr( 1157 _COMPARISON_PROFILE, "discharge_b_one" 1158 ): 1159 message = "Nothing to compare. The profile test may have failed or been skipped" 1160 logger.write_result_to_html_report(message) 1161 pytest.skip(message) 1162 1163 if isinstance(_GENERATED_PROFILE.discharge_b_one, str) or isinstance(_COMPARISON_PROFILE.discharge_b_one, str): 1164 if not isinstance(_GENERATED_PROFILE.discharge_b_one, type(_COMPARISON_PROFILE.discharge_b_one)): 1165 message = "Unable to compare profiles, types of Battery Regulation Information 1 did not match" 1166 logger.write_failure_to_html_report(message) 1167 pytest.fail(message) 1168 1169 comparison = cmp(_GENERATED_PROFILE.discharge_b_one, "==", _COMPARISON_PROFILE.discharge_b_one) 1170 1171 if _GENERATED_PROFILE.discharge_b_one != _COMPARISON_PROFILE.discharge_b_one: 1172 message = f"Battery Regulation Information 1 matched profiles: {comparison}" 1173 logger.write_failure_to_html_report(message) 1174 pytest.fail(message) 1175 1176 message = f"Battery Regulation Information 1 matched: {comparison}" 1177 logger.write_result_to_html_report(message) 1178 return 1179 1180 original_current = _GENERATED_PROFILE.discharge_b_one.get("battery_current") 1181 comparison_current = _COMPARISON_PROFILE.discharge_b_one.get("battery_current") 1182 1183 diff = abs(original_current - comparison_current) 1184 average = abs(original_current + comparison_current) / 2 1185 percentage = round((diff / average), 4) 1186 percent_closeness = 0.1 1187 1188 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 1189 1190 if not math.isclose(original_current, comparison_current, rel_tol=0.1): 1191 logger.write_warning_to_html_report( 1192 f"Battery Regulation Information 1 current percent difference: {comparison}" 1193 ) 1194 logger.write_failure_to_html_report("Current was not within expected range of 10% during discharge") 1195 pytest.fail("Current was not within expected range of 10% during charge") 1196 1197 logger.write_result_to_html_report(f"Battery regulation Information 1 current percent difference: {comparison}") 1198 logger.write_result_to_html_report("Current was within expected range of 10% during discharge") 1199 1200 @staticmethod 1201 def get_battery_regulation_one() -> str | dict[str, int | Any]: 1202 """Obtain the Battery Regulation One information""" 1203 battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03]) 1204 logger.write_result_to_html_report("<span style='font-weight: bold'>Battery Regulation Information 1</span>") 1205 1206 with CANBus(BATTERY_CHANNEL) as bus: 1207 battery_regulation_one: str | dict[str, int | Any] 1208 if battery_frame := bus.process_call(battery_regulation_one_request): 1209 if battery_frame.pgn.id != PGN["PropB_03", [32]].id: 1210 message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}" 1211 logger.write_warning_to_html_report(message) 1212 battery_regulation_one = BadStates.WRONG_PGN.name 1213 else: 1214 battery_regulation_one = { 1215 "battery_voltage": battery_frame.data[0], 1216 "open_circuit_voltage": battery_frame.data[1], 1217 "battery_current": battery_frame.data[2], 1218 "maximum_charge_current": battery_frame.data[3], 1219 } 1220 1221 for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data): 1222 1223 if spn.name == "Reserved": 1224 continue 1225 1226 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1227 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1228 1229 if spn.name == "Battery Current": 1230 if -0.1 <= elem <= 0.1: 1231 message = ( 1232 "Current was not detected. The power supply leads may not be fully " 1233 "connected, or the fuse has blown" 1234 ) 1235 logger.write_warning_to_html_report(message) 1236 return "No Current" 1237 1238 else: 1239 message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)" 1240 logger.write_warning_to_html_report(message) 1241 battery_regulation_one = BadStates.NO_RESPONSE.name 1242 1243 return battery_regulation_one 1244 1245 @staticmethod 1246 def get_battery_regulation_two() -> str | dict[str, int | Any]: 1247 """Obtain the Battery Regulation Two information""" 1248 battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04]) 1249 logger.write_result_to_html_report("<span style='font-weight: bold'>Battery Regulation Information 2</span>") 1250 1251 with CANBus(BATTERY_CHANNEL) as bus: 1252 battery_regulation_two: str | dict[str, int | Any] 1253 if battery_frame_two := bus.process_call(battery_regulation_two_request): 1254 if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id: 1255 message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}" 1256 logger.write_warning_to_html_report(message) 1257 battery_regulation_two = BadStates.NO_RESPONSE.name 1258 else: 1259 battery_regulation_two = { 1260 "contractor_state": battery_frame_two.data[0], 1261 "charge_capability_state": battery_frame_two.data[1], 1262 "bus_voltage_request": battery_frame_two.data[3], 1263 "transportability_soc": battery_frame_two.data[4], 1264 } 1265 1266 for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data): 1267 1268 if spn.name == "Reserved": 1269 continue 1270 1271 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1272 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1273 1274 else: 1275 message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)" 1276 logger.write_warning_to_html_report(message) 1277 battery_regulation_two = BadStates.NO_RESPONSE.name 1278 1279 return battery_regulation_two
Retrieve and validate battery regulation information
866 @pytest.mark.profile 867 def test_charge_power_supply(self) -> None: 868 """ 869 | Description | Check Battery Information after Charge | 870 | :------------------- | :--------------------------------------------------------------- | 871 | GitHub Issue | turnaroundfactor/BMS-HW-Test#597 | 872 | Instructions | 1. Request Battery State of Charge </br>\ 873 2. Charge battery at 0.1A for 1 minute </br>\ 874 3. Check & save Battery Regulation 1 & 2 information </br>\ 875 4. Log Response | 876 | Estimated Duration | 2 minutes | 877 """ 878 879 battery_calculation_one = CANFrame(pgn=PGN["RQST"], data=[PGN["PropB_02", [32]].id]) 880 881 with CANBus(BATTERY_CHANNEL) as bus: 882 if battery_calculation_response := bus.process_call(battery_calculation_one): 883 if battery_calculation_response.pgn.id != PGN["PropB_02", [32]].id: 884 message = ( 885 f"Expected PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name}) " 886 f" but received PGN {battery_calculation_response.pgn.id}" 887 ) 888 logger.write_warning_to_html_report(message) 889 _GENERATED_PROFILE.test_power_supply = BadStates.WRONG_PGN.name 890 pytest.skip(message) 891 892 for spn, elem in zip(PGN["PropB_02"].data_field, battery_calculation_response.data): 893 low_range = 0 894 high_range = 95 895 if spn.name == "Battery State of Charge": 896 comparison = f"{cmp(elem, '>=', low_range, '%')} " f"and {cmp(elem, '<=', high_range, '%' )}" 897 898 if not low_range <= elem <= high_range: 899 message = ( 900 f"Skipping Test: {spn.name} is {elem}%, " 901 f"which is not within expected range: {comparison}" 902 ) 903 logger.write_warning_to_html_report(message) 904 _GENERATED_PROFILE.test_power_supply = BadStates.SKIPPED_TEST.name 905 pytest.skip(message) 906 else: 907 message = f"{spn.name} is {elem}%" 908 logger.write_result_to_html_report(message) 909 break 910 else: 911 message = f"Did not receive response for PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name})" 912 logger.write_warning_to_html_report(message) 913 _GENERATED_PROFILE.test_power_supply = BadStates.NO_RESPONSE.name 914 pytest.skip(message) 915 916 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") and not hasattr( 917 _GENERATED_PROFILE, "battery_regulation_two" 918 ): 919 default_b_one = self.get_battery_regulation_one() 920 default_b_two = self.get_battery_regulation_two() 921 else: 922 default_b_one = _GENERATED_PROFILE.battery_regulation_one 923 default_b_two = _GENERATED_PROFILE.battery_regulation_two 924 925 if isinstance(default_b_one, str) or isinstance(default_b_two, str): 926 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 927 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 928 pytest.skip("Missing valid Battery Regulation One and Two info") 929 930 logger.write_result_to_html_report("Charging battery") 931 try: 932 cyber_charger = charger.Charger() 933 except RuntimeError: 934 message = "Could not communicate with power supply" 935 logger.write_warning_to_html_report(message) 936 pytest.fail(message) 937 938 try: 939 with cyber_charger(0.1, float(default_b_two.get("bus_voltage_request", 0.0))): 940 logger.write_info_to_report("Battery Regulation One Values from Charge") 941 updated_b_one = self.get_battery_regulation_one() 942 logger.write_info_to_report("Battery Regulation Two Values from Charge") 943 updated_b_two = self.get_battery_regulation_two() 944 945 if isinstance(updated_b_one, str): 946 if updated_b_one == "No Current": 947 _GENERATED_PROFILE.test_charge_power_supply = "Current not detected" 948 raise RuntimeError 949 950 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 951 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 952 pytest.skip("Missing valid Battery Regulation One and Two info") 953 954 _GENERATED_PROFILE.charge_b_one = updated_b_one 955 _GENERATED_PROFILE.charge_b_two = updated_b_two 956 957 except RuntimeError: 958 if cyber_charger.resource: 959 if hasattr(cyber_charger.resource, "close"): 960 cyber_charger.resource.close() 961 message = "Could not communicate with power supply or current not detected" 962 logger.write_warning_to_html_report(message) 963 964 if not hasattr(_GENERATED_PROFILE, "test_charge_power_supply"): 965 _GENERATED_PROFILE.test_charge_power_supply = "Could not communicate" 966 pytest.fail(message) 967 968 except AttributeError: 969 message = "Could not communicate with power supply" 970 logger.write_warning_to_html_report(message) 971 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 972 pytest.fail(message)
| Description | Check Battery Information after Charge |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#597 |
| Instructions | 1. Request Battery State of Charge 2. Charge battery at 0.1A for 1 minute 3. Check & save Battery Regulation 1 & 2 information 4. Log Response |
| Estimated Duration | 2 minutes |
974 def test_charge_comparison(self): 975 """ 976 | Description | Compare Power Supply Charge values | 977 | :------------------- | :--------------------------------------------------------------- | 978 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 979 | Instructions | 1. Check if charge Battery Current is within 10% of initial value| 980 | Pass / Fail Criteria | Pass if charged value is within 10% | 981 | Estimated Duration | 1 second | 982 """ 983 984 if hasattr(_GENERATED_PROFILE, "test_power_supply") or not hasattr(_GENERATED_PROFILE, "charge_b_one"): 985 message = "Nothing to compare. Profile test may have failed or been skipped" 986 logger.write_result_to_html_report(message) 987 pytest.skip(message) 988 989 if hasattr(_COMPARISON_PROFILE, "test_power_supply") or not hasattr(_COMPARISON_PROFILE, "charge_b_one"): 990 message = "Nothing to compare. Profile test may have failed or been skipped" 991 logger.write_result_to_html_report(message) 992 pytest.skip(message) 993 994 if isinstance(_GENERATED_PROFILE.charge_b_one, str) or isinstance(_COMPARISON_PROFILE.charge_b_one, str): 995 if not isinstance(_GENERATED_PROFILE.charge_b_one, type(_COMPARISON_PROFILE.charge_b_one)): 996 message = "Unable to compare profiles, types of Battery Regulation Information 1 did not match" 997 logger.write_failure_to_html_report(message) 998 pytest.fail(message) 999 1000 comparison = cmp(_GENERATED_PROFILE.charge_b_one, "==", _COMPARISON_PROFILE.charge_b_one) 1001 1002 if _GENERATED_PROFILE.charge_b_one != _COMPARISON_PROFILE.charge_b_one: 1003 message = f"Battery Regulation Information 1 matched profiles: {comparison}" 1004 logger.write_failure_to_html_report(message) 1005 pytest.fail(message) 1006 1007 message = f"Battery Regulation Information 1 matched: {comparison}" 1008 logger.write_result_to_html_report(message) 1009 return 1010 1011 original_current = _GENERATED_PROFILE.charge_b_one.get("battery_current") 1012 comparison_current = _COMPARISON_PROFILE.charge_b_one.get("battery_current") 1013 1014 diff = abs(original_current - comparison_current) 1015 average = abs(original_current + comparison_current) / 2 1016 percentage = round((diff / average), 4) 1017 percent_closeness = 0.1 1018 1019 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 1020 1021 if not math.isclose(original_current, comparison_current, rel_tol=0.1): 1022 logger.write_warning_to_html_report( 1023 f"Battery Regulation Information 1 current percent difference: {comparison}" 1024 ) 1025 logger.write_failure_to_html_report("Current was not within expected range during charge") 1026 pytest.fail("Current was not within expected range during charge") 1027 1028 logger.write_result_to_html_report(f"Battery Regulation Information 1 current percent difference: {comparison}") 1029 logger.write_result_to_html_report("Current was within expected range during charge")
| Description | Compare Power Supply Charge values |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if charge Battery Current is within 10% of initial value |
| Pass / Fail Criteria | Pass if charged value is within 10% |
| Estimated Duration | 1 second |
1031 @pytest.mark.profile 1032 def test_discharge_power_supply(self) -> None: 1033 """ 1034 | Description | Check Battery Information after Discharge | 1035 | :------------------- | :--------------------------------------------------------------- | 1036 | GitHub Issue | turnaroundfactor/BMS-HW-Test#597 | 1037 | Instructions | 1. Request Battery State of Charge </br>\ 1038 2. Discharge battery at -0.1A for 1 minute </br>\ 1039 3. Check & save Battery Regulation 1 & 2 information </br>\ 1040 4. Log Response | 1041 | Estimated Duration | 2 minutes | 1042 """ 1043 1044 battery_calculation_one = CANFrame(pgn=PGN["RQST"], data=[PGN["PropB_02", [32]].id]) 1045 1046 with CANBus(BATTERY_CHANNEL) as bus: 1047 if battery_calculation_response := bus.process_call(battery_calculation_one): 1048 if battery_calculation_response.pgn.id != PGN["PropB_02", [32]].id: 1049 message = ( 1050 f"Expected PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name}) " 1051 f" but received PGN {battery_calculation_response.pgn.id}" 1052 ) 1053 logger.write_warning_to_html_report(message) 1054 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.WRONG_PGN.name 1055 return 1056 1057 for spn, elem in zip(PGN["PropB_02"].data_field, battery_calculation_response.data): 1058 if spn.name == "Battery State of Charge": 1059 low_range = 5 1060 high_range = 100 1061 comparison = f"{cmp(elem, '>=', low_range, '%')} and {cmp(elem, '<=', high_range, '%' )}" 1062 1063 if not low_range <= elem <= high_range: 1064 message = ( 1065 f"Skipping Test: {spn.name} is {elem}%, " 1066 f"which is not within expected range: {comparison}" 1067 ) 1068 logger.write_warning_to_html_report(message) 1069 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.SKIPPED_TEST.name 1070 1071 pytest.skip(message) 1072 else: 1073 message = f"{spn.name} is {elem}%" 1074 logger.write_result_to_html_report(message) 1075 break 1076 else: 1077 message = f"Did not receive response for PGN {PGN['PropB_02', [32]].id} ({PGN['PropB_02', [32]].name})" 1078 logger.write_warning_to_html_report(message) 1079 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.NO_RESPONSE.name 1080 pytest.skip(message) 1081 1082 if not hasattr(_GENERATED_PROFILE, "battery_regulation_one") and not hasattr( 1083 _GENERATED_PROFILE, "battery_regulation_two" 1084 ): 1085 default_b_one = self.get_battery_regulation_one() 1086 default_b_two = self.get_battery_regulation_two() 1087 else: 1088 default_b_one = _GENERATED_PROFILE.battery_regulation_one 1089 default_b_two = _GENERATED_PROFILE.battery_regulation_two 1090 1091 if isinstance(default_b_one, str) or isinstance(default_b_two, str): 1092 _GENERATED_PROFILE.test_discharge_power_supply = BadStates.INVALID_RESPONSE.name 1093 1094 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 1095 pytest.skip("Missing valid Battery Regulation One and Two info") 1096 1097 logger.write_result_to_html_report("Discharging battery") 1098 try: 1099 cyber_charger = charger.Charger() 1100 except RuntimeError: 1101 message = "Could not communicate with power supply" 1102 logger.write_warning_to_html_report(message) 1103 pytest.fail(message) 1104 try: 1105 with cyber_charger(-0.1): 1106 logger.write_info_to_report("Battery Regulation One Values from Discharge") 1107 updated_b_one = self.get_battery_regulation_one() 1108 logger.write_info_to_report("Battery Regulation Two Values from Discharge") 1109 updated_b_two = self.get_battery_regulation_two() 1110 1111 if isinstance(updated_b_one, str): 1112 if updated_b_one == "No Current": 1113 _GENERATED_PROFILE.test_discharge_power_supply = "Current not detected" 1114 raise RuntimeError 1115 1116 _GENERATED_PROFILE.test_power_supply = BadStates.INVALID_RESPONSE.name 1117 logger.write_warning_to_html_report("Missing valid Battery Regulation One and Two info") 1118 pytest.skip("Missing valid Battery Regulation One and Two info") 1119 1120 _GENERATED_PROFILE.discharge_b_one = updated_b_one 1121 _GENERATED_PROFILE.discharge_b_two = updated_b_two 1122 1123 except RuntimeError: 1124 if cyber_charger.resource: 1125 if hasattr(cyber_charger.resource, "close"): 1126 cyber_charger.resource.close() 1127 message = "Could not communicate with power supply or current not detected" 1128 logger.write_warning_to_html_report(message) 1129 if not hasattr(_GENERATED_PROFILE, "test_charge_power_supply"): 1130 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 1131 pytest.fail(message) 1132 1133 except AttributeError: 1134 message = "Could not communicate with power supply" 1135 logger.write_warning_to_html_report(message) 1136 _GENERATED_PROFILE.test_discharge_power_supply = "Could not communicate" 1137 pytest.fail(message)
| Description | Check Battery Information after Discharge |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#597 |
| Instructions | 1. Request Battery State of Charge 2. Discharge battery at -0.1A for 1 minute 3. Check & save Battery Regulation 1 & 2 information 4. Log Response |
| Estimated Duration | 2 minutes |
1139 def test_discharge_comparison(self): 1140 """ 1141 | Description | Compare Power Supply Discharge values | 1142 | :------------------- | :--------------------------------------------------------------- | 1143 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1144 | Instructions | 1. Check if discharge Battery Current is within 10% of initial value| 1145 | Pass / Fail Criteria | Pass if discharged value is within 10% | 1146 | Estimated Duration | 1 second | 1147 """ 1148 1149 if hasattr(_GENERATED_PROFILE, "test_discharge_power_supply") or not hasattr( 1150 _GENERATED_PROFILE, "discharge_b_one" 1151 ): 1152 message = "Nothing to compare. The profile test may have failed or been skipped" 1153 logger.write_result_to_html_report(message) 1154 pytest.skip(message) 1155 1156 if hasattr(_COMPARISON_PROFILE, "test_discharge_power_supply") or not hasattr( 1157 _COMPARISON_PROFILE, "discharge_b_one" 1158 ): 1159 message = "Nothing to compare. The profile test may have failed or been skipped" 1160 logger.write_result_to_html_report(message) 1161 pytest.skip(message) 1162 1163 if isinstance(_GENERATED_PROFILE.discharge_b_one, str) or isinstance(_COMPARISON_PROFILE.discharge_b_one, str): 1164 if not isinstance(_GENERATED_PROFILE.discharge_b_one, type(_COMPARISON_PROFILE.discharge_b_one)): 1165 message = "Unable to compare profiles, types of Battery Regulation Information 1 did not match" 1166 logger.write_failure_to_html_report(message) 1167 pytest.fail(message) 1168 1169 comparison = cmp(_GENERATED_PROFILE.discharge_b_one, "==", _COMPARISON_PROFILE.discharge_b_one) 1170 1171 if _GENERATED_PROFILE.discharge_b_one != _COMPARISON_PROFILE.discharge_b_one: 1172 message = f"Battery Regulation Information 1 matched profiles: {comparison}" 1173 logger.write_failure_to_html_report(message) 1174 pytest.fail(message) 1175 1176 message = f"Battery Regulation Information 1 matched: {comparison}" 1177 logger.write_result_to_html_report(message) 1178 return 1179 1180 original_current = _GENERATED_PROFILE.discharge_b_one.get("battery_current") 1181 comparison_current = _COMPARISON_PROFILE.discharge_b_one.get("battery_current") 1182 1183 diff = abs(original_current - comparison_current) 1184 average = abs(original_current + comparison_current) / 2 1185 percentage = round((diff / average), 4) 1186 percent_closeness = 0.1 1187 1188 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 1189 1190 if not math.isclose(original_current, comparison_current, rel_tol=0.1): 1191 logger.write_warning_to_html_report( 1192 f"Battery Regulation Information 1 current percent difference: {comparison}" 1193 ) 1194 logger.write_failure_to_html_report("Current was not within expected range of 10% during discharge") 1195 pytest.fail("Current was not within expected range of 10% during charge") 1196 1197 logger.write_result_to_html_report(f"Battery regulation Information 1 current percent difference: {comparison}") 1198 logger.write_result_to_html_report("Current was within expected range of 10% during discharge")
| Description | Compare Power Supply Discharge values |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if discharge Battery Current is within 10% of initial value |
| Pass / Fail Criteria | Pass if discharged value is within 10% |
| Estimated Duration | 1 second |
1200 @staticmethod 1201 def get_battery_regulation_one() -> str | dict[str, int | Any]: 1202 """Obtain the Battery Regulation One information""" 1203 battery_regulation_one_request = CANFrame(pgn=PGN["RQST"], data=[0xFF03]) 1204 logger.write_result_to_html_report("<span style='font-weight: bold'>Battery Regulation Information 1</span>") 1205 1206 with CANBus(BATTERY_CHANNEL) as bus: 1207 battery_regulation_one: str | dict[str, int | Any] 1208 if battery_frame := bus.process_call(battery_regulation_one_request): 1209 if battery_frame.pgn.id != PGN["PropB_03", [32]].id: 1210 message = f"Expected {PGN['PropB_03', [32]].id} 0xFF03, but received PGN {battery_frame.pgn.id}" 1211 logger.write_warning_to_html_report(message) 1212 battery_regulation_one = BadStates.WRONG_PGN.name 1213 else: 1214 battery_regulation_one = { 1215 "battery_voltage": battery_frame.data[0], 1216 "open_circuit_voltage": battery_frame.data[1], 1217 "battery_current": battery_frame.data[2], 1218 "maximum_charge_current": battery_frame.data[3], 1219 } 1220 1221 for spn, elem in zip(PGN["PropB_03"].data_field, battery_frame.data): 1222 1223 if spn.name == "Reserved": 1224 continue 1225 1226 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1227 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1228 1229 if spn.name == "Battery Current": 1230 if -0.1 <= elem <= 0.1: 1231 message = ( 1232 "Current was not detected. The power supply leads may not be fully " 1233 "connected, or the fuse has blown" 1234 ) 1235 logger.write_warning_to_html_report(message) 1236 return "No Current" 1237 1238 else: 1239 message = f"Did not receive response for PGN {PGN['PropB_03', [32]].id} (Battery Regulation One)" 1240 logger.write_warning_to_html_report(message) 1241 battery_regulation_one = BadStates.NO_RESPONSE.name 1242 1243 return battery_regulation_one
Obtain the Battery Regulation One information
1245 @staticmethod 1246 def get_battery_regulation_two() -> str | dict[str, int | Any]: 1247 """Obtain the Battery Regulation Two information""" 1248 battery_regulation_two_request = CANFrame(pgn=PGN["RQST"], data=[0xFF04]) 1249 logger.write_result_to_html_report("<span style='font-weight: bold'>Battery Regulation Information 2</span>") 1250 1251 with CANBus(BATTERY_CHANNEL) as bus: 1252 battery_regulation_two: str | dict[str, int | Any] 1253 if battery_frame_two := bus.process_call(battery_regulation_two_request): 1254 if battery_frame_two.pgn.id != PGN["PropB_04", [32]].id: 1255 message = f"Expected PGN {PGN['PropB_04', [32]].id}, but received PGN {battery_frame_two.pgn.id}" 1256 logger.write_warning_to_html_report(message) 1257 battery_regulation_two = BadStates.NO_RESPONSE.name 1258 else: 1259 battery_regulation_two = { 1260 "contractor_state": battery_frame_two.data[0], 1261 "charge_capability_state": battery_frame_two.data[1], 1262 "bus_voltage_request": battery_frame_two.data[3], 1263 "transportability_soc": battery_frame_two.data[4], 1264 } 1265 1266 for spn, elem in zip(PGN["PropB_04"].data_field, battery_frame_two.data): 1267 1268 if spn.name == "Reserved": 1269 continue 1270 1271 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1272 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1273 1274 else: 1275 message = f"Did not receive response for PGN {PGN['PropB_04', [32]].id} (Battery Regulation Two)" 1276 logger.write_warning_to_html_report(message) 1277 battery_regulation_two = BadStates.NO_RESPONSE.name 1278 1279 return battery_regulation_two
Obtain the Battery Regulation Two information
1282class TestConfigurationStateMessage: 1283 """Confirms Configuration State Message matches profile after factory reset""" 1284 1285 @pytest.mark.profile 1286 def test_read(self) -> None: 1287 """ 1288 | Description | Checks if Configuration State Message matches profile | 1289 | :------------------- | :--------------------------------------------------------------- | 1290 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1291 | Instructions | 1. Request Configuration State </br>\ 1292 2. Check if Configuration State matches default values </br>\ 1293 3. Log results | 1294 | Estimated Duration | 21 seconds | 1295 """ 1296 1297 configuration_state_request = CANFrame(pgn=PGN["Request"], data=[PGN["Configuration State Message 1"].id]) 1298 with CANBus(BATTERY_CHANNEL) as bus: 1299 if configuration_frame := bus.process_call(configuration_state_request): 1300 if configuration_frame.pgn.id != PGN["Configuration State Message 1", [32]].id: 1301 message = ( 1302 f"Received PGN {configuration_frame.pgn.id}, not PGN " 1303 f"{PGN['Configuration State Message 1', [32]].id} " 1304 ) 1305 logger.write_failure_to_html_report(message) 1306 _GENERATED_PROFILE.configuration_state_message = BadStates.WRONG_PGN.name 1307 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1308 return 1309 1310 data = configuration_frame.data 1311 pgn_data_field = PGN["Configuration State Message 1"].data_field 1312 1313 if len(data) != len(pgn_data_field): 1314 message = ( 1315 f"Expected {len(pgn_data_field)} data fields, " 1316 f"got {len(data)}. Data is missing from response" 1317 ) 1318 logger.write_warning_to_html_report(message) 1319 _GENERATED_PROFILE.configuration_state_message = BadStates.WRONG_PACKETS.name 1320 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1321 return 1322 1323 logger.write_result_to_html_report("<span style='font-weight: bold'>Configuration State Message</span>") 1324 1325 for spn, elem in zip(pgn_data_field, data): 1326 if spn.name == "Reserved": 1327 continue 1328 1329 if not 0 <= elem <= 3: 1330 message = ( 1331 f"{spn.name}: {cmp(elem, '>=', 0, f'({spn.data_type(elem)})')} and " 1332 f"{cmp(elem, '<=', 3, f'({spn.data_type(elem)})')}" 1333 ) 1334 logger.write_warning_to_html_report(message) 1335 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1336 continue 1337 1338 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1339 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1340 1341 if spn.name in ("Battery Battle-override State", "Standby State", "Configure VPMS Function State"): 1342 message = ( 1343 f"{spn.name}: {cmp(elem, '==', 0, f'({spn.data_type(elem)})', f'({spn.data_type(0)})')}" 1344 ) 1345 if elem != 0: 1346 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1347 logger.write_warning_to_html_report(message) 1348 1349 if spn.name in ("Automated Heater Function State", "Contactor(s) Control State"): 1350 message = ( 1351 f"{spn.name}: {cmp(elem, '==', 1, f'({spn.data_type(elem)})', f'({spn.data_type(1)})')}" 1352 ) 1353 if elem != 1: 1354 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1355 logger.write_warning_to_html_report(message) 1356 1357 configuration_state_message = { 1358 "dormant_1_state": data[0], 1359 "dormant_2_state": data[1], 1360 "master_power_switch": data[2], 1361 "configuration_pin_1_state": data[3], 1362 "configuration_pin_2_state": data[4], 1363 "configuration_pin_3_state": data[5], 1364 "configuration_pin_4_state": data[6], 1365 "configuration_pin_5_state": data[7], 1366 "configuration_pin_6_state": data[8], 1367 "virtual_master_power_switch_state": data[9], 1368 "battery_battle_override_state": data[11], 1369 "maintenance_state": data[12], 1370 "automated_heater_function_state": data[13], 1371 "battery_heater_state": data[14], 1372 "contractor_control_state": data[15], 1373 "standby_state": data[16], 1374 "baud_rate_overwrite_state": data[18], 1375 "position_identity_overwrite_state": data[19], 1376 "configure_vpms_function_state": data[20], 1377 "pulse_power_control_state": data[21], 1378 } 1379 1380 _GENERATED_PROFILE.configuration_state_message = configuration_state_message 1381 1382 if not hasattr(_GENERATED_PROFILE, "configuration_state_specification"): 1383 _GENERATED_PROFILE.configuration_state_specification = "valid" 1384 1385 else: 1386 message = ( 1387 f"Did not receive a response for PGN {PGN['Configuration State Message 1', [32]].id} " 1388 f"(Configuration State Message 1)" 1389 ) 1390 _GENERATED_PROFILE.configuration_state_message = BadStates.NO_RESPONSE.name 1391 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1392 logger.write_warning_to_html_report(message) 1393 1394 def test_comparison(self) -> None: 1395 """ 1396 | Description | Compare Configuration State Message 1 values | 1397 | :------------------- | :--------------------------------------------------------------- | 1398 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1399 | Instructions | 1. Check if Configuration State Message 1 values match | 1400 | Pass / Fail Criteria | Pass if Configuration State Message 1 values match | 1401 | Estimated Duration | 1 second | 1402 """ 1403 failed_test = False 1404 if not hasattr(_GENERATED_PROFILE, "configuration_state_message") or not hasattr( 1405 _COMPARISON_PROFILE, "configuration_state_message" 1406 ): 1407 message = "Nothing to compare. The profile test may have failed or been skipped" 1408 logger.write_result_to_html_report(message) 1409 pytest.skip(message) 1410 1411 failed_categories = [] 1412 1413 logger.write_result_to_html_report( 1414 "<span style='font-weight: bold'>Configuration State Message Comparisons</span>" 1415 ) 1416 1417 if isinstance(_GENERATED_PROFILE.configuration_state_message, str) or isinstance( 1418 _COMPARISON_PROFILE.configuration_state_message, str 1419 ): 1420 if not isinstance( 1421 _GENERATED_PROFILE.configuration_state_message, type(_COMPARISON_PROFILE.configuration_state_message) 1422 ): 1423 message = "Unable to compare profiles, Configuration State Message types did not match" 1424 logger.write_failure_to_html_report(message) 1425 pytest.fail(message) 1426 1427 comparison = cmp( 1428 _GENERATED_PROFILE.configuration_state_message, "==", _COMPARISON_PROFILE.configuration_state_message 1429 ) 1430 1431 if _GENERATED_PROFILE.configuration_state_message != _COMPARISON_PROFILE.configuration_state_message: 1432 message = f"Configuration State Message did not match: {comparison}" 1433 logger.write_failure_to_html_report(message) 1434 pytest.fail(message) 1435 1436 message = f"Configuration State Message matched: {comparison}" 1437 logger.write_result_to_html_report(message) 1438 else: 1439 1440 for key, value in _GENERATED_PROFILE.configuration_state_message.items(): 1441 key_text = key.replace("_", " ").title() 1442 comparison = cmp(value, "==", _COMPARISON_PROFILE.configuration_state_message[key]) 1443 message = f"{key_text}: {comparison}" 1444 1445 if value != _COMPARISON_PROFILE.configuration_state_message[key]: 1446 logger.write_warning_to_html_report(message) 1447 failed_categories.append(key_text) 1448 else: 1449 logger.write_result_to_html_report(message) 1450 1451 fail_length = len(failed_categories) 1452 1453 if fail_length > 0: 1454 categories = "data values" if fail_length > 1 else "data value" 1455 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 1456 logger.write_failure_to_html_report(message) 1457 failed_test = True 1458 1459 if not hasattr(_GENERATED_PROFILE, "configuration_state_specification") or not hasattr( 1460 _COMPARISON_PROFILE, "configuration_state_specification" 1461 ): 1462 logger.write_warning_to_html_report("Missing specification information") 1463 else: 1464 comparison = cmp( 1465 _GENERATED_PROFILE.configuration_state_specification, 1466 "==", 1467 _COMPARISON_PROFILE.configuration_state_specification, 1468 ) 1469 if ( 1470 _GENERATED_PROFILE.configuration_state_specification 1471 != _COMPARISON_PROFILE.configuration_state_specification 1472 ): 1473 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1474 failed_test = True 1475 else: 1476 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1477 1478 if failed_test: 1479 message = "Comparisons did not match in profiles" 1480 logger.write_failure_to_html_report(message) 1481 pytest.fail(message) 1482 1483 logger.write_result_to_html_report("Configuration State Message 1 values successfully matched")
Confirms Configuration State Message matches profile after factory reset
1285 @pytest.mark.profile 1286 def test_read(self) -> None: 1287 """ 1288 | Description | Checks if Configuration State Message matches profile | 1289 | :------------------- | :--------------------------------------------------------------- | 1290 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1291 | Instructions | 1. Request Configuration State </br>\ 1292 2. Check if Configuration State matches default values </br>\ 1293 3. Log results | 1294 | Estimated Duration | 21 seconds | 1295 """ 1296 1297 configuration_state_request = CANFrame(pgn=PGN["Request"], data=[PGN["Configuration State Message 1"].id]) 1298 with CANBus(BATTERY_CHANNEL) as bus: 1299 if configuration_frame := bus.process_call(configuration_state_request): 1300 if configuration_frame.pgn.id != PGN["Configuration State Message 1", [32]].id: 1301 message = ( 1302 f"Received PGN {configuration_frame.pgn.id}, not PGN " 1303 f"{PGN['Configuration State Message 1', [32]].id} " 1304 ) 1305 logger.write_failure_to_html_report(message) 1306 _GENERATED_PROFILE.configuration_state_message = BadStates.WRONG_PGN.name 1307 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1308 return 1309 1310 data = configuration_frame.data 1311 pgn_data_field = PGN["Configuration State Message 1"].data_field 1312 1313 if len(data) != len(pgn_data_field): 1314 message = ( 1315 f"Expected {len(pgn_data_field)} data fields, " 1316 f"got {len(data)}. Data is missing from response" 1317 ) 1318 logger.write_warning_to_html_report(message) 1319 _GENERATED_PROFILE.configuration_state_message = BadStates.WRONG_PACKETS.name 1320 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1321 return 1322 1323 logger.write_result_to_html_report("<span style='font-weight: bold'>Configuration State Message</span>") 1324 1325 for spn, elem in zip(pgn_data_field, data): 1326 if spn.name == "Reserved": 1327 continue 1328 1329 if not 0 <= elem <= 3: 1330 message = ( 1331 f"{spn.name}: {cmp(elem, '>=', 0, f'({spn.data_type(elem)})')} and " 1332 f"{cmp(elem, '<=', 3, f'({spn.data_type(elem)})')}" 1333 ) 1334 logger.write_warning_to_html_report(message) 1335 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1336 continue 1337 1338 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1339 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1340 1341 if spn.name in ("Battery Battle-override State", "Standby State", "Configure VPMS Function State"): 1342 message = ( 1343 f"{spn.name}: {cmp(elem, '==', 0, f'({spn.data_type(elem)})', f'({spn.data_type(0)})')}" 1344 ) 1345 if elem != 0: 1346 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1347 logger.write_warning_to_html_report(message) 1348 1349 if spn.name in ("Automated Heater Function State", "Contactor(s) Control State"): 1350 message = ( 1351 f"{spn.name}: {cmp(elem, '==', 1, f'({spn.data_type(elem)})', f'({spn.data_type(1)})')}" 1352 ) 1353 if elem != 1: 1354 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1355 logger.write_warning_to_html_report(message) 1356 1357 configuration_state_message = { 1358 "dormant_1_state": data[0], 1359 "dormant_2_state": data[1], 1360 "master_power_switch": data[2], 1361 "configuration_pin_1_state": data[3], 1362 "configuration_pin_2_state": data[4], 1363 "configuration_pin_3_state": data[5], 1364 "configuration_pin_4_state": data[6], 1365 "configuration_pin_5_state": data[7], 1366 "configuration_pin_6_state": data[8], 1367 "virtual_master_power_switch_state": data[9], 1368 "battery_battle_override_state": data[11], 1369 "maintenance_state": data[12], 1370 "automated_heater_function_state": data[13], 1371 "battery_heater_state": data[14], 1372 "contractor_control_state": data[15], 1373 "standby_state": data[16], 1374 "baud_rate_overwrite_state": data[18], 1375 "position_identity_overwrite_state": data[19], 1376 "configure_vpms_function_state": data[20], 1377 "pulse_power_control_state": data[21], 1378 } 1379 1380 _GENERATED_PROFILE.configuration_state_message = configuration_state_message 1381 1382 if not hasattr(_GENERATED_PROFILE, "configuration_state_specification"): 1383 _GENERATED_PROFILE.configuration_state_specification = "valid" 1384 1385 else: 1386 message = ( 1387 f"Did not receive a response for PGN {PGN['Configuration State Message 1', [32]].id} " 1388 f"(Configuration State Message 1)" 1389 ) 1390 _GENERATED_PROFILE.configuration_state_message = BadStates.NO_RESPONSE.name 1391 _GENERATED_PROFILE.configuration_state_specification = "invalid" 1392 logger.write_warning_to_html_report(message)
| Description | Checks if Configuration State Message matches profile |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Request Configuration State 2. Check if Configuration State matches default values 3. Log results |
| Estimated Duration | 21 seconds |
1394 def test_comparison(self) -> None: 1395 """ 1396 | Description | Compare Configuration State Message 1 values | 1397 | :------------------- | :--------------------------------------------------------------- | 1398 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1399 | Instructions | 1. Check if Configuration State Message 1 values match | 1400 | Pass / Fail Criteria | Pass if Configuration State Message 1 values match | 1401 | Estimated Duration | 1 second | 1402 """ 1403 failed_test = False 1404 if not hasattr(_GENERATED_PROFILE, "configuration_state_message") or not hasattr( 1405 _COMPARISON_PROFILE, "configuration_state_message" 1406 ): 1407 message = "Nothing to compare. The profile test may have failed or been skipped" 1408 logger.write_result_to_html_report(message) 1409 pytest.skip(message) 1410 1411 failed_categories = [] 1412 1413 logger.write_result_to_html_report( 1414 "<span style='font-weight: bold'>Configuration State Message Comparisons</span>" 1415 ) 1416 1417 if isinstance(_GENERATED_PROFILE.configuration_state_message, str) or isinstance( 1418 _COMPARISON_PROFILE.configuration_state_message, str 1419 ): 1420 if not isinstance( 1421 _GENERATED_PROFILE.configuration_state_message, type(_COMPARISON_PROFILE.configuration_state_message) 1422 ): 1423 message = "Unable to compare profiles, Configuration State Message types did not match" 1424 logger.write_failure_to_html_report(message) 1425 pytest.fail(message) 1426 1427 comparison = cmp( 1428 _GENERATED_PROFILE.configuration_state_message, "==", _COMPARISON_PROFILE.configuration_state_message 1429 ) 1430 1431 if _GENERATED_PROFILE.configuration_state_message != _COMPARISON_PROFILE.configuration_state_message: 1432 message = f"Configuration State Message did not match: {comparison}" 1433 logger.write_failure_to_html_report(message) 1434 pytest.fail(message) 1435 1436 message = f"Configuration State Message matched: {comparison}" 1437 logger.write_result_to_html_report(message) 1438 else: 1439 1440 for key, value in _GENERATED_PROFILE.configuration_state_message.items(): 1441 key_text = key.replace("_", " ").title() 1442 comparison = cmp(value, "==", _COMPARISON_PROFILE.configuration_state_message[key]) 1443 message = f"{key_text}: {comparison}" 1444 1445 if value != _COMPARISON_PROFILE.configuration_state_message[key]: 1446 logger.write_warning_to_html_report(message) 1447 failed_categories.append(key_text) 1448 else: 1449 logger.write_result_to_html_report(message) 1450 1451 fail_length = len(failed_categories) 1452 1453 if fail_length > 0: 1454 categories = "data values" if fail_length > 1 else "data value" 1455 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 1456 logger.write_failure_to_html_report(message) 1457 failed_test = True 1458 1459 if not hasattr(_GENERATED_PROFILE, "configuration_state_specification") or not hasattr( 1460 _COMPARISON_PROFILE, "configuration_state_specification" 1461 ): 1462 logger.write_warning_to_html_report("Missing specification information") 1463 else: 1464 comparison = cmp( 1465 _GENERATED_PROFILE.configuration_state_specification, 1466 "==", 1467 _COMPARISON_PROFILE.configuration_state_specification, 1468 ) 1469 if ( 1470 _GENERATED_PROFILE.configuration_state_specification 1471 != _COMPARISON_PROFILE.configuration_state_specification 1472 ): 1473 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1474 failed_test = True 1475 else: 1476 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1477 1478 if failed_test: 1479 message = "Comparisons did not match in profiles" 1480 logger.write_failure_to_html_report(message) 1481 pytest.fail(message) 1482 1483 logger.write_result_to_html_report("Configuration State Message 1 values successfully matched")
| Description | Compare Configuration State Message 1 values |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if Configuration State Message 1 values match |
| Pass / Fail Criteria | Pass if Configuration State Message 1 values match |
| Estimated Duration | 1 second |
1486def na_maintenance_mode(bus: CANBus) -> bool: 1487 """This function will place battery in Maintenance Mode with Reset Value of "3".""" 1488 1489 maintenance_mode = CANFrame( # Memory access only works in maintenance mode 1490 destination_address=_GENERATED_PROFILE.address, 1491 pgn=PGN["PropA"], 1492 data=[0, 0, 1, 1, 1, 3, 1, 3, 0, -1], 1493 ) 1494 1495 bus.send_message(maintenance_mode.message()) # Enter maintenance mode 1496 if response := bus.read_input(): 1497 ack_frame = CANFrame.decode(response.arbitration_id, response.data) 1498 if ack_frame.pgn.id != 0xE800 and ack_frame.data[0] != 0: 1499 logger.write_warning_to_html_report("Unable to enter maintenance mode") 1500 return False 1501 return True 1502 1503 message = "Received no maintenance mode response" 1504 logger.write_warning_to_html_report(message) 1505 return False
This function will place battery in Maintenance Mode with Reset Value of "3".
1508class TestNameManagementMessage: 1509 """This will test the mandatory NAME Management commands""" 1510 1511 @pytest.mark.profile 1512 def test_name_management_command(self) -> None: 1513 """ 1514 | Description | Test Name Management Command | 1515 | :------------------- | :--------------------------------------------------------------- | 1516 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1517 | Instructions | 1. Get NAME information from Address Claimed </br>\ 1518 2. Change ECU value </br>\ 1519 3. Test Name Management Command </br>\ 1520 4. Check Values updated </br>\ 1521 5. Log results | 1522 | Estimated Duration | 21 seconds | 1523 """ 1524 1525 old_ecu_value = 0 1526 new_ecu_value = 0 1527 checksum_value = 0 1528 adopt_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1529 address_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 1530 1531 with CANBus(BATTERY_CHANNEL) as bus: 1532 entered_maintenance_mode = na_maintenance_mode(bus) 1533 if not entered_maintenance_mode: 1534 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1535 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1536 1537 # Name Management Test 1538 if address_claimed := bus.process_call(address_request): 1539 if address_claimed.pgn.id != PGN["Address Claimed", [32]].id: 1540 Errors.unexpected_packet("Address Claimed", address_claimed) 1541 message = f"Unexpected packet PGN {address_claimed.pgn.id} was received" 1542 logger.write_warning_to_html_report(message) 1543 _GENERATED_PROFILE.name_management_ecu_value = BadStates.WRONG_PACKETS.name 1544 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1545 return 1546 1547 checksum_value = sum(address_claimed.packed_data) & 0xFF 1548 old_ecu_value = address_claimed.data[2] 1549 if old_ecu_value > 0: 1550 new_ecu_value = 0 1551 else: 1552 new_ecu_value = 1 1553 else: 1554 message = f"No response received for PGN {[PGN['Address Claimed', [32]].id]}" 1555 logger.write_warning_to_html_report(message) 1556 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1557 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1558 return 1559 1560 if not checksum_value: 1561 message = "Could not condense Name into bits for NAME Management" 1562 logger.write_warning_to_html_report(message) 1563 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1564 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1565 return 1566 1567 request_data: list[float] = [ 1568 checksum_value, 1569 1, 1570 0, 1571 1, 1572 1, 1573 1, 1574 1, 1575 1, 1576 1, 1577 0, 1578 1, 1579 1, 1580 new_ecu_value, 1581 1, 1582 1, 1583 1, 1584 1, 1585 1, 1586 1, 1587 1, 1588 ] 1589 name_management_set_name = CANFrame( 1590 destination_address=_GENERATED_PROFILE.address, pgn=PGN["NAME Management Message"], data=request_data 1591 ) 1592 1593 if pending_response := bus.process_call(name_management_set_name): 1594 if pending_response.pgn.id != PGN["NAME Management Message", [32]].id: 1595 if pending_response.pgn.id == PGN["Acknowledgement", [32]].id: 1596 message = ( 1597 f"Battery may not support NAME Management, received PGN " 1598 f"{pending_response.pgn.id} ({spn_types.acknowledgement(pending_response.data[0])})" 1599 ) 1600 logger.write_warning_to_html_report(message) 1601 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NACK.name 1602 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1603 1604 else: 1605 Errors.unexpected_packet("NAME Management Message", pending_response) 1606 message = f"Unexpected PGN {pending_response.pgn.id} received" 1607 logger.write_warning_to_html_report(message) 1608 _GENERATED_PROFILE.name_management_ecu_value = BadStates.WRONG_PGN.name 1609 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1610 1611 return 1612 1613 if pending_response.data[0] == 3: 1614 message = ( 1615 f"Message was unsuccessful, received error: " 1616 f"{spn_types.name_error_code(pending_response.data[0])}" 1617 ) 1618 logger.write_warning_to_html_report(message) 1619 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1620 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1621 return 1622 1623 if pending_response.data[12] != new_ecu_value: 1624 message = f"ECU Value was not changed from {old_ecu_value} to {new_ecu_value}" 1625 logger.write_warning_to_html_report(message) 1626 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1627 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1628 return 1629 else: 1630 message = ( 1631 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1632 ) 1633 logger.write_warning_to_html_report(message) 1634 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1635 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1636 return 1637 1638 adopt_name_request = CANFrame( 1639 destination_address=_GENERATED_PROFILE.address, 1640 pgn=PGN["NAME Management Message"], 1641 data=adopt_name_request_data, 1642 ) 1643 1644 bus.send_message(adopt_name_request.message()) 1645 1646 current_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1647 current_name_request = CANFrame( 1648 destination_address=_GENERATED_PROFILE.address, 1649 pgn=PGN["NAME Management Message"], 1650 data=current_name_request_data, 1651 ) 1652 1653 if name_management_response := bus.process_call(current_name_request): 1654 if name_management_response.data[12] != new_ecu_value: 1655 message = ( 1656 f"Name's ECU Instance was not updated after Name Management Changes, " 1657 f"{cmp(name_management_response.data[12], '==', new_ecu_value)}" 1658 ) 1659 logger.write_warning_to_html_report(message) 1660 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1661 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1662 return 1663 else: 1664 message = ( 1665 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1666 ) 1667 logger.write_warning_to_html_report(message) 1668 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1669 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1670 return 1671 1672 if new_address_claimed := bus.process_call(address_request): 1673 if new_address_claimed.data[2] != new_ecu_value + 1: 1674 message = ( 1675 f"Address Claimed was not updated after Name Management Changes. " 1676 f"{cmp(new_address_claimed.data[2], '==', new_ecu_value + 1)}" 1677 ) 1678 logger.write_warning_to_html_report(message) 1679 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1680 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1681 return 1682 1683 _GENERATED_PROFILE.name_management_ecu_value = address_claimed.data[2] 1684 logger.write_result_to_html_report( 1685 f"Name Management Command was successful. " 1686 f"ECU Instance was changed from {old_ecu_value} to {new_ecu_value}" 1687 ) 1688 _GENERATED_PROFILE.name_management_ecu_specification = "valid" 1689 1690 @pytest.mark.profile 1691 def test_wrong_name_management_data(self): 1692 """ 1693 | Description | Test Incorrect Data for NAME Management Command | 1694 | :------------------- | :--------------------------------------------------------------- | 1695 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1696 | Instructions | 1. Provide wrong Checksum value for Name Command </br>\ 1697 2. Check receive Checksum error code as response </br>\ 1698 3. Log results | 1699 | Estimated Duration | 1 second | 1700 """ 1701 wrong_checksum = 0 1702 new_ecu_value = 0 1703 1704 current_name_request_data = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1705 current_name_request = CANFrame( 1706 destination_address=_GENERATED_PROFILE.address, 1707 pgn=PGN["NAME Management Message"], 1708 data=current_name_request_data, 1709 ) 1710 1711 with CANBus(BATTERY_CHANNEL) as bus: 1712 # Test Checksum Error -- Should return Error Code: 3 1713 in_maintenance_mode = na_maintenance_mode(bus) 1714 1715 if not in_maintenance_mode: 1716 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1717 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1718 1719 if current_name := bus.process_call(current_name_request): 1720 if current_name.pgn.id != PGN["NAME Management Message", [32]].id: 1721 if current_name.pgn.id == PGN["Acknowledgement", [32]].id: 1722 message = ( 1723 f"Battery may not support NAME Management, received PGN " 1724 f"{current_name.pgn.id} ({spn_types.acknowledgement(current_name.data[0])})" 1725 ) 1726 logger.write_warning_to_html_report(message) 1727 _GENERATED_PROFILE.wrong_name_data = BadStates.NACK.name 1728 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1729 return 1730 Errors.unexpected_packet("NAME Management Message", current_name) 1731 message = f"Unexpected packet was received: PGN {current_name.pgn.id}" 1732 logger.write_warning_to_html_report(message) 1733 _GENERATED_PROFILE.wrong_name_data = BadStates.WRONG_PACKETS.name 1734 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1735 return 1736 1737 wrong_checksum = sum(current_name.packed_data) & 0xFF 1738 old_ecu_value = current_name.data[12] 1739 if old_ecu_value > 0: 1740 new_ecu_value = 0 1741 else: 1742 new_ecu_value = 1 1743 else: 1744 message = ( 1745 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1746 ) 1747 logger.write_warning_to_html_report(message) 1748 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1749 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1750 return 1751 1752 request_data = [wrong_checksum, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, new_ecu_value, 1, 1, 1, 1, 1, 1, 1] 1753 name_management_set_name = CANFrame( 1754 destination_address=_GENERATED_PROFILE.address, pgn=PGN["NAME Management Message"], data=request_data 1755 ) 1756 1757 if pending_response := bus.process_call(name_management_set_name): 1758 if pending_response.pgn.id != PGN["NAME Management Message", [32]].id: 1759 if pending_response.pgn.id == PGN["Acknowledgement", [32]].id: 1760 message = ( 1761 f"Battery may not support NAME Management, received PGN {pending_response.pgn.id} " 1762 f"({spn_types.acknowledgement(pending_response.data[0])})" 1763 ) 1764 logger.write_warning_to_html_report(message) 1765 _GENERATED_PROFILE.wrong_name_data = BadStates.NACK.name 1766 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1767 return 1768 1769 Errors.unexpected_packet("NAME Management Message", pending_response) 1770 message = f"Unexpected packet was received: {pending_response.pgn.id}" 1771 logger.write_warning_to_html_report(message) 1772 _GENERATED_PROFILE.wrong_name_data = BadStates.WRONG_PGN.name 1773 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1774 return 1775 1776 if pending_response.data[0] != 3: 1777 message = cmp( 1778 pending_response.data[0], 1779 "==", 1780 3, 1781 f"({spn_types.name_error_code(pending_response.data[0])})", 1782 f"({spn_types.name_error_code(3)})", 1783 ) 1784 logger.write_warning_to_html_report( 1785 f"PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message) " 1786 f"updated NAME Management with incorrect checksum value" 1787 ) 1788 logger.write_warning_to_html_report(message) 1789 _GENERATED_PROFILE.wrong_name_data = "Incorrect checksum value" 1790 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1791 return 1792 else: 1793 message = ( 1794 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1795 ) 1796 logger.write_warning_to_html_report(message) 1797 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1798 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1799 return 1800 1801 logger.write_result_to_html_report( 1802 "NAME Management successfully did not process request with incorrect checksum value" 1803 ) 1804 _GENERATED_PROFILE.wrong_name_data = "Passed" 1805 _GENERATED_PROFILE.wrong_name_specification = "valid" 1806 1807 def test_name_comparison(self) -> None: 1808 """ 1809 | Description | Compare if Name Management ECU values were changed | 1810 | :------------------- | :--------------------------------------------------------------- | 1811 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1812 | Instructions | 1. Check if Name Management ECU values matched in profiles | 1813 | Pass / Fail Criteria | Pass if Name Management ECU values match | 1814 | Estimated Duration | 1 second | 1815 """ 1816 failed_test = False 1817 if not hasattr(_GENERATED_PROFILE, "name_management_ecu_value") or not hasattr( 1818 _COMPARISON_PROFILE, "name_management_ecu_value" 1819 ): 1820 message = "Nothing to compare. The profile test may have failed or been skipped" 1821 logger.write_result_to_html_report(message) 1822 pytest.skip(message) 1823 1824 comparison = cmp( 1825 _GENERATED_PROFILE.name_management_ecu_value, "==", _COMPARISON_PROFILE.name_management_ecu_value 1826 ) 1827 1828 if _GENERATED_PROFILE.name_management_ecu_value != _COMPARISON_PROFILE.name_management_ecu_value: 1829 comparison = cmp( 1830 _GENERATED_PROFILE.name_management_ecu_value, "==", _COMPARISON_PROFILE.name_management_ecu_value 1831 ) 1832 message = f"Name Management ECU values did not match: {comparison}" 1833 logger.write_failure_to_html_report(message) 1834 failed_test = True 1835 1836 logger.write_result_to_html_report(f"Name Management ECU values matched: {comparison}") 1837 1838 if not hasattr(_GENERATED_PROFILE, "name_management_ecu_specification") or not hasattr( 1839 _COMPARISON_PROFILE, "name_management_ecu_specification" 1840 ): 1841 logger.write_warning_to_html_report("Missing specification information") 1842 else: 1843 comparison = cmp( 1844 _GENERATED_PROFILE.name_management_ecu_specification, 1845 "==", 1846 _COMPARISON_PROFILE.name_management_ecu_specification, 1847 ) 1848 if ( 1849 _GENERATED_PROFILE.name_management_ecu_specification 1850 != _COMPARISON_PROFILE.name_management_ecu_specification 1851 ): 1852 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1853 failed_test = True 1854 else: 1855 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1856 1857 if failed_test: 1858 message = "Comparisons did not match in profiles" 1859 logger.write_failure_to_html_report(message) 1860 pytest.fail(message) 1861 1862 def test_wrong_name_comparison(self) -> None: 1863 """ 1864 | Description | Compare if Name Management values were changed with wrong data | 1865 | :------------------- | :--------------------------------------------------------------- | 1866 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1867 | Instructions | 1. Check if Name Management ECU values matched in profiles | 1868 | Pass / Fail Criteria | Pass if Name Management ECU values match | 1869 | Estimated Duration | 1 second | 1870 """ 1871 failed_test = False 1872 if not hasattr(_GENERATED_PROFILE, "wrong_name_data") or not hasattr(_COMPARISON_PROFILE, "wrong_name_data"): 1873 message = "Nothing to compare. The profile test may have failed or been skipped" 1874 logger.write_result_to_html_report(message) 1875 pytest.skip(message) 1876 1877 if _GENERATED_PROFILE.wrong_name_data != _COMPARISON_PROFILE.wrong_name_data: 1878 comparison = f"{_GENERATED_PROFILE.wrong_name_data} ≠ { _COMPARISON_PROFILE.wrong_name_data}" 1879 message = f"Name Management values did not match from Wrong Name Management test: {comparison}" 1880 logger.write_failure_to_html_report(message) 1881 failed_test = True 1882 else: 1883 logger.write_result_to_html_report( 1884 f"Name Management values had same results from Wrong Name Management test: " 1885 f"{_GENERATED_PROFILE.wrong_name_data}" 1886 ) 1887 1888 if not hasattr(_GENERATED_PROFILE, "wrong_name_specification") or not hasattr( 1889 _COMPARISON_PROFILE, "wrong_name_specification" 1890 ): 1891 logger.write_warning_to_html_report("Missing specification information") 1892 else: 1893 comparison = cmp( 1894 _GENERATED_PROFILE.wrong_name_specification, 1895 "==", 1896 _COMPARISON_PROFILE.wrong_name_specification, 1897 ) 1898 if _GENERATED_PROFILE.wrong_name_specification != _COMPARISON_PROFILE.wrong_name_specification: 1899 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1900 failed_test = True 1901 else: 1902 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1903 1904 if failed_test: 1905 message = "Comparisons did not match in profiles" 1906 logger.write_failure_to_html_report(message) 1907 pytest.fail(message)
This will test the mandatory NAME Management commands
1511 @pytest.mark.profile 1512 def test_name_management_command(self) -> None: 1513 """ 1514 | Description | Test Name Management Command | 1515 | :------------------- | :--------------------------------------------------------------- | 1516 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1517 | Instructions | 1. Get NAME information from Address Claimed </br>\ 1518 2. Change ECU value </br>\ 1519 3. Test Name Management Command </br>\ 1520 4. Check Values updated </br>\ 1521 5. Log results | 1522 | Estimated Duration | 21 seconds | 1523 """ 1524 1525 old_ecu_value = 0 1526 new_ecu_value = 0 1527 checksum_value = 0 1528 adopt_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1529 address_request = CANFrame(pgn=PGN["Request"], data=[PGN["Address Claimed"].id]) 1530 1531 with CANBus(BATTERY_CHANNEL) as bus: 1532 entered_maintenance_mode = na_maintenance_mode(bus) 1533 if not entered_maintenance_mode: 1534 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1535 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1536 1537 # Name Management Test 1538 if address_claimed := bus.process_call(address_request): 1539 if address_claimed.pgn.id != PGN["Address Claimed", [32]].id: 1540 Errors.unexpected_packet("Address Claimed", address_claimed) 1541 message = f"Unexpected packet PGN {address_claimed.pgn.id} was received" 1542 logger.write_warning_to_html_report(message) 1543 _GENERATED_PROFILE.name_management_ecu_value = BadStates.WRONG_PACKETS.name 1544 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1545 return 1546 1547 checksum_value = sum(address_claimed.packed_data) & 0xFF 1548 old_ecu_value = address_claimed.data[2] 1549 if old_ecu_value > 0: 1550 new_ecu_value = 0 1551 else: 1552 new_ecu_value = 1 1553 else: 1554 message = f"No response received for PGN {[PGN['Address Claimed', [32]].id]}" 1555 logger.write_warning_to_html_report(message) 1556 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1557 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1558 return 1559 1560 if not checksum_value: 1561 message = "Could not condense Name into bits for NAME Management" 1562 logger.write_warning_to_html_report(message) 1563 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1564 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1565 return 1566 1567 request_data: list[float] = [ 1568 checksum_value, 1569 1, 1570 0, 1571 1, 1572 1, 1573 1, 1574 1, 1575 1, 1576 1, 1577 0, 1578 1, 1579 1, 1580 new_ecu_value, 1581 1, 1582 1, 1583 1, 1584 1, 1585 1, 1586 1, 1587 1, 1588 ] 1589 name_management_set_name = CANFrame( 1590 destination_address=_GENERATED_PROFILE.address, pgn=PGN["NAME Management Message"], data=request_data 1591 ) 1592 1593 if pending_response := bus.process_call(name_management_set_name): 1594 if pending_response.pgn.id != PGN["NAME Management Message", [32]].id: 1595 if pending_response.pgn.id == PGN["Acknowledgement", [32]].id: 1596 message = ( 1597 f"Battery may not support NAME Management, received PGN " 1598 f"{pending_response.pgn.id} ({spn_types.acknowledgement(pending_response.data[0])})" 1599 ) 1600 logger.write_warning_to_html_report(message) 1601 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NACK.name 1602 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1603 1604 else: 1605 Errors.unexpected_packet("NAME Management Message", pending_response) 1606 message = f"Unexpected PGN {pending_response.pgn.id} received" 1607 logger.write_warning_to_html_report(message) 1608 _GENERATED_PROFILE.name_management_ecu_value = BadStates.WRONG_PGN.name 1609 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1610 1611 return 1612 1613 if pending_response.data[0] == 3: 1614 message = ( 1615 f"Message was unsuccessful, received error: " 1616 f"{spn_types.name_error_code(pending_response.data[0])}" 1617 ) 1618 logger.write_warning_to_html_report(message) 1619 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1620 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1621 return 1622 1623 if pending_response.data[12] != new_ecu_value: 1624 message = f"ECU Value was not changed from {old_ecu_value} to {new_ecu_value}" 1625 logger.write_warning_to_html_report(message) 1626 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1627 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1628 return 1629 else: 1630 message = ( 1631 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1632 ) 1633 logger.write_warning_to_html_report(message) 1634 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1635 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1636 return 1637 1638 adopt_name_request = CANFrame( 1639 destination_address=_GENERATED_PROFILE.address, 1640 pgn=PGN["NAME Management Message"], 1641 data=adopt_name_request_data, 1642 ) 1643 1644 bus.send_message(adopt_name_request.message()) 1645 1646 current_name_request_data: list[float] = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1647 current_name_request = CANFrame( 1648 destination_address=_GENERATED_PROFILE.address, 1649 pgn=PGN["NAME Management Message"], 1650 data=current_name_request_data, 1651 ) 1652 1653 if name_management_response := bus.process_call(current_name_request): 1654 if name_management_response.data[12] != new_ecu_value: 1655 message = ( 1656 f"Name's ECU Instance was not updated after Name Management Changes, " 1657 f"{cmp(name_management_response.data[12], '==', new_ecu_value)}" 1658 ) 1659 logger.write_warning_to_html_report(message) 1660 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1661 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1662 return 1663 else: 1664 message = ( 1665 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1666 ) 1667 logger.write_warning_to_html_report(message) 1668 _GENERATED_PROFILE.name_management_ecu_value = BadStates.NO_RESPONSE.name 1669 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1670 return 1671 1672 if new_address_claimed := bus.process_call(address_request): 1673 if new_address_claimed.data[2] != new_ecu_value + 1: 1674 message = ( 1675 f"Address Claimed was not updated after Name Management Changes. " 1676 f"{cmp(new_address_claimed.data[2], '==', new_ecu_value + 1)}" 1677 ) 1678 logger.write_warning_to_html_report(message) 1679 _GENERATED_PROFILE.name_management_ecu_value = BadStates.INVALID_RESPONSE.name 1680 _GENERATED_PROFILE.name_management_ecu_specification = "invalid" 1681 return 1682 1683 _GENERATED_PROFILE.name_management_ecu_value = address_claimed.data[2] 1684 logger.write_result_to_html_report( 1685 f"Name Management Command was successful. " 1686 f"ECU Instance was changed from {old_ecu_value} to {new_ecu_value}" 1687 ) 1688 _GENERATED_PROFILE.name_management_ecu_specification = "valid"
| Description | Test Name Management Command |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#389 |
| Instructions | 1. Get NAME information from Address Claimed 2. Change ECU value 3. Test Name Management Command 4. Check Values updated 5. Log results |
| Estimated Duration | 21 seconds |
1690 @pytest.mark.profile 1691 def test_wrong_name_management_data(self): 1692 """ 1693 | Description | Test Incorrect Data for NAME Management Command | 1694 | :------------------- | :--------------------------------------------------------------- | 1695 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1696 | Instructions | 1. Provide wrong Checksum value for Name Command </br>\ 1697 2. Check receive Checksum error code as response </br>\ 1698 3. Log results | 1699 | Estimated Duration | 1 second | 1700 """ 1701 wrong_checksum = 0 1702 new_ecu_value = 0 1703 1704 current_name_request_data = [255, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1705 current_name_request = CANFrame( 1706 destination_address=_GENERATED_PROFILE.address, 1707 pgn=PGN["NAME Management Message"], 1708 data=current_name_request_data, 1709 ) 1710 1711 with CANBus(BATTERY_CHANNEL) as bus: 1712 # Test Checksum Error -- Should return Error Code: 3 1713 in_maintenance_mode = na_maintenance_mode(bus) 1714 1715 if not in_maintenance_mode: 1716 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1717 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1718 1719 if current_name := bus.process_call(current_name_request): 1720 if current_name.pgn.id != PGN["NAME Management Message", [32]].id: 1721 if current_name.pgn.id == PGN["Acknowledgement", [32]].id: 1722 message = ( 1723 f"Battery may not support NAME Management, received PGN " 1724 f"{current_name.pgn.id} ({spn_types.acknowledgement(current_name.data[0])})" 1725 ) 1726 logger.write_warning_to_html_report(message) 1727 _GENERATED_PROFILE.wrong_name_data = BadStates.NACK.name 1728 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1729 return 1730 Errors.unexpected_packet("NAME Management Message", current_name) 1731 message = f"Unexpected packet was received: PGN {current_name.pgn.id}" 1732 logger.write_warning_to_html_report(message) 1733 _GENERATED_PROFILE.wrong_name_data = BadStates.WRONG_PACKETS.name 1734 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1735 return 1736 1737 wrong_checksum = sum(current_name.packed_data) & 0xFF 1738 old_ecu_value = current_name.data[12] 1739 if old_ecu_value > 0: 1740 new_ecu_value = 0 1741 else: 1742 new_ecu_value = 1 1743 else: 1744 message = ( 1745 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1746 ) 1747 logger.write_warning_to_html_report(message) 1748 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1749 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1750 return 1751 1752 request_data = [wrong_checksum, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, new_ecu_value, 1, 1, 1, 1, 1, 1, 1] 1753 name_management_set_name = CANFrame( 1754 destination_address=_GENERATED_PROFILE.address, pgn=PGN["NAME Management Message"], data=request_data 1755 ) 1756 1757 if pending_response := bus.process_call(name_management_set_name): 1758 if pending_response.pgn.id != PGN["NAME Management Message", [32]].id: 1759 if pending_response.pgn.id == PGN["Acknowledgement", [32]].id: 1760 message = ( 1761 f"Battery may not support NAME Management, received PGN {pending_response.pgn.id} " 1762 f"({spn_types.acknowledgement(pending_response.data[0])})" 1763 ) 1764 logger.write_warning_to_html_report(message) 1765 _GENERATED_PROFILE.wrong_name_data = BadStates.NACK.name 1766 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1767 return 1768 1769 Errors.unexpected_packet("NAME Management Message", pending_response) 1770 message = f"Unexpected packet was received: {pending_response.pgn.id}" 1771 logger.write_warning_to_html_report(message) 1772 _GENERATED_PROFILE.wrong_name_data = BadStates.WRONG_PGN.name 1773 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1774 return 1775 1776 if pending_response.data[0] != 3: 1777 message = cmp( 1778 pending_response.data[0], 1779 "==", 1780 3, 1781 f"({spn_types.name_error_code(pending_response.data[0])})", 1782 f"({spn_types.name_error_code(3)})", 1783 ) 1784 logger.write_warning_to_html_report( 1785 f"PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message) " 1786 f"updated NAME Management with incorrect checksum value" 1787 ) 1788 logger.write_warning_to_html_report(message) 1789 _GENERATED_PROFILE.wrong_name_data = "Incorrect checksum value" 1790 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1791 return 1792 else: 1793 message = ( 1794 f"No response received for PGN {PGN['NAME Management Message', [32]].id} (NAME Management Message)" 1795 ) 1796 logger.write_warning_to_html_report(message) 1797 _GENERATED_PROFILE.wrong_name_data = BadStates.NO_RESPONSE.name 1798 _GENERATED_PROFILE.wrong_name_specification = "invalid" 1799 return 1800 1801 logger.write_result_to_html_report( 1802 "NAME Management successfully did not process request with incorrect checksum value" 1803 ) 1804 _GENERATED_PROFILE.wrong_name_data = "Passed" 1805 _GENERATED_PROFILE.wrong_name_specification = "valid"
| Description | Test Incorrect Data for NAME Management Command |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#389 |
| Instructions | 1. Provide wrong Checksum value for Name Command 2. Check receive Checksum error code as response 3. Log results |
| Estimated Duration | 1 second |
1807 def test_name_comparison(self) -> None: 1808 """ 1809 | Description | Compare if Name Management ECU values were changed | 1810 | :------------------- | :--------------------------------------------------------------- | 1811 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1812 | Instructions | 1. Check if Name Management ECU values matched in profiles | 1813 | Pass / Fail Criteria | Pass if Name Management ECU values match | 1814 | Estimated Duration | 1 second | 1815 """ 1816 failed_test = False 1817 if not hasattr(_GENERATED_PROFILE, "name_management_ecu_value") or not hasattr( 1818 _COMPARISON_PROFILE, "name_management_ecu_value" 1819 ): 1820 message = "Nothing to compare. The profile test may have failed or been skipped" 1821 logger.write_result_to_html_report(message) 1822 pytest.skip(message) 1823 1824 comparison = cmp( 1825 _GENERATED_PROFILE.name_management_ecu_value, "==", _COMPARISON_PROFILE.name_management_ecu_value 1826 ) 1827 1828 if _GENERATED_PROFILE.name_management_ecu_value != _COMPARISON_PROFILE.name_management_ecu_value: 1829 comparison = cmp( 1830 _GENERATED_PROFILE.name_management_ecu_value, "==", _COMPARISON_PROFILE.name_management_ecu_value 1831 ) 1832 message = f"Name Management ECU values did not match: {comparison}" 1833 logger.write_failure_to_html_report(message) 1834 failed_test = True 1835 1836 logger.write_result_to_html_report(f"Name Management ECU values matched: {comparison}") 1837 1838 if not hasattr(_GENERATED_PROFILE, "name_management_ecu_specification") or not hasattr( 1839 _COMPARISON_PROFILE, "name_management_ecu_specification" 1840 ): 1841 logger.write_warning_to_html_report("Missing specification information") 1842 else: 1843 comparison = cmp( 1844 _GENERATED_PROFILE.name_management_ecu_specification, 1845 "==", 1846 _COMPARISON_PROFILE.name_management_ecu_specification, 1847 ) 1848 if ( 1849 _GENERATED_PROFILE.name_management_ecu_specification 1850 != _COMPARISON_PROFILE.name_management_ecu_specification 1851 ): 1852 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1853 failed_test = True 1854 else: 1855 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1856 1857 if failed_test: 1858 message = "Comparisons did not match in profiles" 1859 logger.write_failure_to_html_report(message) 1860 pytest.fail(message)
| Description | Compare if Name Management ECU values were changed |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if Name Management ECU values matched in profiles |
| Pass / Fail Criteria | Pass if Name Management ECU values match |
| Estimated Duration | 1 second |
1862 def test_wrong_name_comparison(self) -> None: 1863 """ 1864 | Description | Compare if Name Management values were changed with wrong data | 1865 | :------------------- | :--------------------------------------------------------------- | 1866 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 1867 | Instructions | 1. Check if Name Management ECU values matched in profiles | 1868 | Pass / Fail Criteria | Pass if Name Management ECU values match | 1869 | Estimated Duration | 1 second | 1870 """ 1871 failed_test = False 1872 if not hasattr(_GENERATED_PROFILE, "wrong_name_data") or not hasattr(_COMPARISON_PROFILE, "wrong_name_data"): 1873 message = "Nothing to compare. The profile test may have failed or been skipped" 1874 logger.write_result_to_html_report(message) 1875 pytest.skip(message) 1876 1877 if _GENERATED_PROFILE.wrong_name_data != _COMPARISON_PROFILE.wrong_name_data: 1878 comparison = f"{_GENERATED_PROFILE.wrong_name_data} ≠ { _COMPARISON_PROFILE.wrong_name_data}" 1879 message = f"Name Management values did not match from Wrong Name Management test: {comparison}" 1880 logger.write_failure_to_html_report(message) 1881 failed_test = True 1882 else: 1883 logger.write_result_to_html_report( 1884 f"Name Management values had same results from Wrong Name Management test: " 1885 f"{_GENERATED_PROFILE.wrong_name_data}" 1886 ) 1887 1888 if not hasattr(_GENERATED_PROFILE, "wrong_name_specification") or not hasattr( 1889 _COMPARISON_PROFILE, "wrong_name_specification" 1890 ): 1891 logger.write_warning_to_html_report("Missing specification information") 1892 else: 1893 comparison = cmp( 1894 _GENERATED_PROFILE.wrong_name_specification, 1895 "==", 1896 _COMPARISON_PROFILE.wrong_name_specification, 1897 ) 1898 if _GENERATED_PROFILE.wrong_name_specification != _COMPARISON_PROFILE.wrong_name_specification: 1899 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 1900 failed_test = True 1901 else: 1902 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 1903 1904 if failed_test: 1905 message = "Comparisons did not match in profiles" 1906 logger.write_failure_to_html_report(message) 1907 pytest.fail(message)
| Description | Compare if Name Management values were changed with wrong data |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if Name Management ECU values matched in profiles |
| Pass / Fail Criteria | Pass if Name Management ECU values match |
| Estimated Duration | 1 second |
1910class TestActiveDiagnosticTroubleCodes: 1911 """Test that Active Diagnostic Trouble Codes are J1939 Compliant""" 1912 1913 @pytest.mark.profile 1914 def test_profile(self) -> None: 1915 """ 1916 | Description | Test Active Diagnostic Trouble Codes (DM1) | 1917 | :------------------- | :--------------------------------------------------------------- | 1918 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1919 | Instructions | 1. Request Active Diagnostic Trouble Codes </br>\ 1920 2. Test Command Addresses command </br>\ 1921 3. Confirm Address was updated </br>\ 1922 4. Log results | 1923 | Estimated Duration | 21 seconds | 1924 """ 1925 dm1_request = CANFrame(pgn=PGN["Request"], data=[PGN["Active Diagnostic Trouble Codes", [32]].id]) 1926 with CANBus(BATTERY_CHANNEL) as bus: 1927 if dm1_frame := bus.process_call(dm1_request): 1928 if dm1_frame.pgn.id != PGN["Active Diagnostic Trouble Codes", [32]].id: 1929 Errors.unexpected_packet("Active Diagnostic Trouble Codes", dm1_frame) 1930 message = f"Unexpected data packet PGN {dm1_frame.pgn.id} was received" 1931 logger.write_warning_to_html_report(message) 1932 _GENERATED_PROFILE.active_diagnostic_trouble_codes = BadStates.WRONG_PGN.name 1933 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 1934 return 1935 1936 logger.write_result_to_html_report( 1937 "<span style='font-weight: bold'>Active Diagnostic Trouble Codes</span>" 1938 ) 1939 1940 dm1_data = dm1_frame.data 1941 pgn_data_field = PGN["Active Diagnostic Trouble Codes"].data_field 1942 1943 for spn, elem in zip(pgn_data_field, dm1_data): 1944 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1945 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1946 high_value = 1 1947 1948 if spn.name in ( 1949 "Protect Lamp", 1950 "Amber Warning Lamp", 1951 "Red Stop Lamp", 1952 "Malfunction Indicator Lamp", 1953 "DTC1.SPN_Conversion_Method", 1954 ): 1955 high_value = 1 1956 1957 if spn.name in ( 1958 "Flash Protect Lamp", 1959 "Flash Amber Warning Lamp", 1960 "Flash Red Stop Lamp", 1961 "Flash Malfunction Indicator Lamp", 1962 ): 1963 high_value = 3 1964 1965 if spn.name == "DTC1.Suspect_Parameter_Number": 1966 high_value = 524287 1967 1968 if spn.name == "DTC1.Failure_Mode_Identifier": 1969 high_value = 31 1970 1971 if spn.name == "DTC1.Occurrence_Count": 1972 high_value = 126 1973 1974 if not 0 <= elem <= high_value: 1975 logger.write_warning_to_html_report( 1976 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 1977 f"{cmp(elem, '<=', high_value, description)}" 1978 ) 1979 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 1980 1981 _GENERATED_PROFILE.active_diagnostic_trouble_codes = { 1982 "protect_lamp": dm1_data[0], 1983 "amber_warning_lamp": dm1_data[1], 1984 "red_stop_lamp": dm1_data[2], 1985 "malfunction_indicator_lamp": dm1_data[3], 1986 "flash_protect_lamp": dm1_data[4], 1987 "flash_amber_warning_lamp": dm1_data[5], 1988 "flash_red_stop_lamp": dm1_data[6], 1989 "flash_malfunction_indicator_lamp": dm1_data[7], 1990 "dtc1_suspect_parameter_number": dm1_data[8], 1991 "dtc1_failure_mode_identifier": dm1_data[9], 1992 "dtc1_occurrence_count": dm1_data[10], 1993 "dtc1_spn_conversion_method": dm1_data[11], 1994 } 1995 1996 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_specification"): 1997 _GENERATED_PROFILE.active_diagnostic_specification = "valid" 1998 1999 else: 2000 message = ( 2001 f"No response was received for mandatory command: " 2002 f"PGN {PGN['Active Diagnostic Trouble Codes', [32]].id} (Active Diagnostic Trouble Codes)" 2003 ) 2004 logger.write_warning_to_html_report(message) 2005 _GENERATED_PROFILE.active_diagnostic_trouble_codes = BadStates.NO_RESPONSE.name 2006 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 2007 2008 def test_comparison(self) -> None: 2009 """ 2010 | Description | Compare if Active Diagnostic Trouble Codes matched | 2011 | :------------------- | :--------------------------------------------------------------- | 2012 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 2013 | Instructions | 1. Check if Active Diagnostic Trouble Codes matched in profiles | 2014 | Pass / Fail Criteria | Pass if Active Diagnostic Trouble Codes match | 2015 | Estimated Duration | 1 second | 2016 """ 2017 failed_test = False 2018 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_trouble_codes") or not hasattr( 2019 _COMPARISON_PROFILE, "active_diagnostic_trouble_codes" 2020 ): 2021 message = "Nothing to compare. The profile test may have failed or been skipped" 2022 logger.write_result_to_html_report(message) 2023 pytest.skip(message) 2024 2025 if isinstance(_GENERATED_PROFILE.active_diagnostic_trouble_codes, str) or isinstance( 2026 _COMPARISON_PROFILE.active_diagnostic_trouble_codes, str 2027 ): 2028 if not isinstance( 2029 _GENERATED_PROFILE.active_diagnostic_trouble_codes, 2030 type(_COMPARISON_PROFILE.active_diagnostic_trouble_codes), 2031 ): 2032 message = "Unable to compare profiles, Active Diagnostic Trouble Codes types did not match" 2033 logger.write_failure_to_html_report(message) 2034 pytest.fail(message) 2035 2036 comparison = cmp( 2037 _GENERATED_PROFILE.active_diagnostic_trouble_codes, 2038 "==", 2039 _COMPARISON_PROFILE.active_diagnostic_trouble_codes, 2040 ) 2041 2042 if ( 2043 _GENERATED_PROFILE.active_diagnostic_trouble_codes 2044 != _COMPARISON_PROFILE.active_diagnostic_trouble_codes 2045 ): 2046 message = f"Active Diagnostic Trouble Codes did not match: {comparison}" 2047 logger.write_failure_to_html_report(message) 2048 pytest.fail(message) 2049 2050 message = f"Active Diagnostic Trouble Codes matched: {comparison}" 2051 logger.write_result_to_html_report(message) 2052 else: 2053 failed_categories = [] 2054 for key, value in _GENERATED_PROFILE.active_diagnostic_trouble_codes.items(): 2055 key_text = key.replace("_", " ").title() 2056 2057 if key == "dtc1_suspect_parameter_number": 2058 key_text = "DTC1.Suspect_Parameter_Number" 2059 2060 if key == "dtc1_failure_mode_identifier": 2061 key_text = "DTC1.Failure_Mode_Identifier" 2062 2063 if key == "dtc1_occurrence_count": 2064 key_text = "DTC1.Occurrence_Count" 2065 2066 if key == "dtc1_spn_conversion_method": 2067 key_text = "DTC1.SPN_Conversion_Method" 2068 2069 comparison = cmp(value, "==", _COMPARISON_PROFILE.active_diagnostic_trouble_codes[key]) 2070 message = f"{key_text}: {comparison}" 2071 2072 if value != _COMPARISON_PROFILE.active_diagnostic_trouble_codes[key]: 2073 logger.write_warning_to_html_report(message) 2074 failed_categories.append(key_text) 2075 else: 2076 logger.write_result_to_html_report(message) 2077 2078 fail_length = len(failed_categories) 2079 2080 if fail_length > 0: 2081 categories = "data values" if fail_length > 1 else "data value" 2082 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2083 logger.write_failure_to_html_report(message) 2084 failed_test = True 2085 else: 2086 logger.write_result_to_html_report("Active Diagnostic Trouble Code values between profiles matched") 2087 2088 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_specification") or not hasattr( 2089 _COMPARISON_PROFILE, "active_diagnostic_specification" 2090 ): 2091 logger.write_warning_to_html_report("Missing specification information") 2092 else: 2093 comparison = cmp( 2094 _GENERATED_PROFILE.active_diagnostic_specification, 2095 "==", 2096 _COMPARISON_PROFILE.active_diagnostic_specification, 2097 ) 2098 if ( 2099 _GENERATED_PROFILE.active_diagnostic_specification 2100 != _COMPARISON_PROFILE.active_diagnostic_specification 2101 ): 2102 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2103 failed_test = True 2104 else: 2105 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2106 2107 if failed_test: 2108 message = "Comparisons did not match in profiles" 2109 logger.write_failure_to_html_report(message) 2110 pytest.fail(message)
Test that Active Diagnostic Trouble Codes are J1939 Compliant
1913 @pytest.mark.profile 1914 def test_profile(self) -> None: 1915 """ 1916 | Description | Test Active Diagnostic Trouble Codes (DM1) | 1917 | :------------------- | :--------------------------------------------------------------- | 1918 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 1919 | Instructions | 1. Request Active Diagnostic Trouble Codes </br>\ 1920 2. Test Command Addresses command </br>\ 1921 3. Confirm Address was updated </br>\ 1922 4. Log results | 1923 | Estimated Duration | 21 seconds | 1924 """ 1925 dm1_request = CANFrame(pgn=PGN["Request"], data=[PGN["Active Diagnostic Trouble Codes", [32]].id]) 1926 with CANBus(BATTERY_CHANNEL) as bus: 1927 if dm1_frame := bus.process_call(dm1_request): 1928 if dm1_frame.pgn.id != PGN["Active Diagnostic Trouble Codes", [32]].id: 1929 Errors.unexpected_packet("Active Diagnostic Trouble Codes", dm1_frame) 1930 message = f"Unexpected data packet PGN {dm1_frame.pgn.id} was received" 1931 logger.write_warning_to_html_report(message) 1932 _GENERATED_PROFILE.active_diagnostic_trouble_codes = BadStates.WRONG_PGN.name 1933 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 1934 return 1935 1936 logger.write_result_to_html_report( 1937 "<span style='font-weight: bold'>Active Diagnostic Trouble Codes</span>" 1938 ) 1939 1940 dm1_data = dm1_frame.data 1941 pgn_data_field = PGN["Active Diagnostic Trouble Codes"].data_field 1942 1943 for spn, elem in zip(pgn_data_field, dm1_data): 1944 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 1945 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 1946 high_value = 1 1947 1948 if spn.name in ( 1949 "Protect Lamp", 1950 "Amber Warning Lamp", 1951 "Red Stop Lamp", 1952 "Malfunction Indicator Lamp", 1953 "DTC1.SPN_Conversion_Method", 1954 ): 1955 high_value = 1 1956 1957 if spn.name in ( 1958 "Flash Protect Lamp", 1959 "Flash Amber Warning Lamp", 1960 "Flash Red Stop Lamp", 1961 "Flash Malfunction Indicator Lamp", 1962 ): 1963 high_value = 3 1964 1965 if spn.name == "DTC1.Suspect_Parameter_Number": 1966 high_value = 524287 1967 1968 if spn.name == "DTC1.Failure_Mode_Identifier": 1969 high_value = 31 1970 1971 if spn.name == "DTC1.Occurrence_Count": 1972 high_value = 126 1973 1974 if not 0 <= elem <= high_value: 1975 logger.write_warning_to_html_report( 1976 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 1977 f"{cmp(elem, '<=', high_value, description)}" 1978 ) 1979 _GENERATED_PROFILE.active_diagnostic_specification = "invalid" 1980 1981 _GENERATED_PROFILE.active_diagnostic_trouble_codes = { 1982 "protect_lamp": dm1_data[0], 1983 "amber_warning_lamp": dm1_data[1], 1984 "red_stop_lamp": dm1_data[2], 1985 "malfunction_indicator_lamp": dm1_data[3], 1986 "flash_protect_lamp": dm1_data[4], 1987 "flash_amber_warning_lamp": dm1_data[5], 1988 "flash_red_stop_lamp": dm1_data[6], 1989 "flash_malfunction_indicator_lamp": dm1_data[7], 1990 "dtc1_suspect_parameter_number": dm1_data[8], 1991 "dtc1_failure_mode_identifier": dm1_data[9], 1992 "dtc1_occurrence_count": dm1_data[10], 1993 "dtc1_spn_conversion_method": dm1_data[11], 1994 } 1995 1996 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_specification"): 1997 _GENERATED_PROFILE.active_diagnostic_specification = "valid" 1998 1999 else: 2000 message = ( 2001 f"No response was received for mandatory command: " 2002 f"PGN {PGN['Active Diagnostic Trouble Codes', [32]].id} (Active Diagnostic Trouble Codes)" 2003 ) 2004 logger.write_warning_to_html_report(message) 2005 _GENERATED_PROFILE.active_diagnostic_trouble_codes = BadStates.NO_RESPONSE.name 2006 _GENERATED_PROFILE.active_diagnostic_specification = "invalid"
| Description | Test Active Diagnostic Trouble Codes (DM1) |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#389 |
| Instructions | 1. Request Active Diagnostic Trouble Codes 2. Test Command Addresses command 3. Confirm Address was updated 4. Log results |
| Estimated Duration | 21 seconds |
2008 def test_comparison(self) -> None: 2009 """ 2010 | Description | Compare if Active Diagnostic Trouble Codes matched | 2011 | :------------------- | :--------------------------------------------------------------- | 2012 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 2013 | Instructions | 1. Check if Active Diagnostic Trouble Codes matched in profiles | 2014 | Pass / Fail Criteria | Pass if Active Diagnostic Trouble Codes match | 2015 | Estimated Duration | 1 second | 2016 """ 2017 failed_test = False 2018 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_trouble_codes") or not hasattr( 2019 _COMPARISON_PROFILE, "active_diagnostic_trouble_codes" 2020 ): 2021 message = "Nothing to compare. The profile test may have failed or been skipped" 2022 logger.write_result_to_html_report(message) 2023 pytest.skip(message) 2024 2025 if isinstance(_GENERATED_PROFILE.active_diagnostic_trouble_codes, str) or isinstance( 2026 _COMPARISON_PROFILE.active_diagnostic_trouble_codes, str 2027 ): 2028 if not isinstance( 2029 _GENERATED_PROFILE.active_diagnostic_trouble_codes, 2030 type(_COMPARISON_PROFILE.active_diagnostic_trouble_codes), 2031 ): 2032 message = "Unable to compare profiles, Active Diagnostic Trouble Codes types did not match" 2033 logger.write_failure_to_html_report(message) 2034 pytest.fail(message) 2035 2036 comparison = cmp( 2037 _GENERATED_PROFILE.active_diagnostic_trouble_codes, 2038 "==", 2039 _COMPARISON_PROFILE.active_diagnostic_trouble_codes, 2040 ) 2041 2042 if ( 2043 _GENERATED_PROFILE.active_diagnostic_trouble_codes 2044 != _COMPARISON_PROFILE.active_diagnostic_trouble_codes 2045 ): 2046 message = f"Active Diagnostic Trouble Codes did not match: {comparison}" 2047 logger.write_failure_to_html_report(message) 2048 pytest.fail(message) 2049 2050 message = f"Active Diagnostic Trouble Codes matched: {comparison}" 2051 logger.write_result_to_html_report(message) 2052 else: 2053 failed_categories = [] 2054 for key, value in _GENERATED_PROFILE.active_diagnostic_trouble_codes.items(): 2055 key_text = key.replace("_", " ").title() 2056 2057 if key == "dtc1_suspect_parameter_number": 2058 key_text = "DTC1.Suspect_Parameter_Number" 2059 2060 if key == "dtc1_failure_mode_identifier": 2061 key_text = "DTC1.Failure_Mode_Identifier" 2062 2063 if key == "dtc1_occurrence_count": 2064 key_text = "DTC1.Occurrence_Count" 2065 2066 if key == "dtc1_spn_conversion_method": 2067 key_text = "DTC1.SPN_Conversion_Method" 2068 2069 comparison = cmp(value, "==", _COMPARISON_PROFILE.active_diagnostic_trouble_codes[key]) 2070 message = f"{key_text}: {comparison}" 2071 2072 if value != _COMPARISON_PROFILE.active_diagnostic_trouble_codes[key]: 2073 logger.write_warning_to_html_report(message) 2074 failed_categories.append(key_text) 2075 else: 2076 logger.write_result_to_html_report(message) 2077 2078 fail_length = len(failed_categories) 2079 2080 if fail_length > 0: 2081 categories = "data values" if fail_length > 1 else "data value" 2082 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2083 logger.write_failure_to_html_report(message) 2084 failed_test = True 2085 else: 2086 logger.write_result_to_html_report("Active Diagnostic Trouble Code values between profiles matched") 2087 2088 if not hasattr(_GENERATED_PROFILE, "active_diagnostic_specification") or not hasattr( 2089 _COMPARISON_PROFILE, "active_diagnostic_specification" 2090 ): 2091 logger.write_warning_to_html_report("Missing specification information") 2092 else: 2093 comparison = cmp( 2094 _GENERATED_PROFILE.active_diagnostic_specification, 2095 "==", 2096 _COMPARISON_PROFILE.active_diagnostic_specification, 2097 ) 2098 if ( 2099 _GENERATED_PROFILE.active_diagnostic_specification 2100 != _COMPARISON_PROFILE.active_diagnostic_specification 2101 ): 2102 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2103 failed_test = True 2104 else: 2105 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2106 2107 if failed_test: 2108 message = "Comparisons did not match in profiles" 2109 logger.write_failure_to_html_report(message) 2110 pytest.fail(message)
| Description | Compare if Active Diagnostic Trouble Codes matched |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if Active Diagnostic Trouble Codes matched in profiles |
| Pass / Fail Criteria | Pass if Active Diagnostic Trouble Codes match |
| Estimated Duration | 1 second |
2239class TestVehicleElectricalPower: 2240 """Test Vehicle Electrical Power command is J1939 Compliant""" 2241 2242 @pytest.mark.profile 2243 def test_profile(self) -> None: 2244 """ 2245 | Description | Test Vehicle Electrical Power #5 (VEP5) command | 2246 | :------------------- | :--------------------------------------------------------------- | 2247 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 2248 | Instructions | 1. Send Vehicle Electrical Power #5 command </br>\ 2249 2. Check values in data </br>\ 2250 3. Log response | 2251 | Estimated Duration | 21 seconds | 2252 """ 2253 2254 with CANBus(BATTERY_CHANNEL) as bus: 2255 vehicle_electrical_power_request = CANFrame( 2256 destination_address=_GENERATED_PROFILE.address, 2257 pgn=PGN["Request"], 2258 data=[PGN["Vehicle Electrical Power #5"].id], 2259 ) 2260 if vehicle_electrical_power_response := bus.process_call(vehicle_electrical_power_request): 2261 if vehicle_electrical_power_response.pgn.id != PGN["Vehicle Electrical Power #5", [32]].id: 2262 if vehicle_electrical_power_response.pgn.id == 59392: 2263 message = ( 2264 f"Received PGN {vehicle_electrical_power_response.pgn.id} " 2265 f"({spn_types.acknowledgement(vehicle_electrical_power_response.data[0])}), " 2266 f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} ' 2267 ) 2268 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.NACK.name 2269 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2270 else: 2271 message = ( 2272 f"Received PGN {vehicle_electrical_power_response.pgn.id} " 2273 f"{vehicle_electrical_power_response.pgn.short_name}, " 2274 f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} ' 2275 ) 2276 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.WRONG_PGN.name 2277 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2278 logger.write_warning_to_html_report(message) 2279 return 2280 else: 2281 message = ( 2282 f"Battery did not respond to PGN {PGN['Vehicle Electrical Power #5', [32]].id} " 2283 f"Vehicle Electrical Power #5 request" 2284 ) 2285 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.NO_RESPONSE.name 2286 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2287 logger.write_warning_to_html_report(message) 2288 return 2289 2290 vep5_data = vehicle_electrical_power_response.data 2291 vep5_pgn = PGN["Vehicle Electrical Power #5"].data_field 2292 2293 logger.write_result_to_html_report("<span style='font-weight: bold'>Vehicle Electrical Power #5</span>") 2294 2295 for spn, elem in zip(vep5_pgn, vep5_data): 2296 if spn.name in ["Reserved", "SLI Battery Pack State of Charge"]: 2297 continue 2298 2299 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 2300 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 2301 high_value = 1.0 2302 2303 if spn.name == "SLI Battery Pack Capacity": 2304 high_value = 64255 2305 2306 if spn.name == "SLI Battery Pack Health": 2307 high_value = 125 2308 2309 if spn.name == "SLI Cranking Predicted Minimum Battery Voltage": 2310 high_value = 50 2311 2312 if not 0 <= elem <= high_value: 2313 logger.write_warning_to_html_report( 2314 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 2315 f"{cmp(elem, '<=', high_value, description)}" 2316 ) 2317 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2318 2319 vehicle_electrical_power = { 2320 "sli_battery_pack_capacity": vep5_data[1], 2321 "sli_battery_pack_health": vep5_data[2], 2322 "sli_cranking_predicted_minimum_battery_voltage": vep5_data[3], 2323 } 2324 2325 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_specification"): 2326 _GENERATED_PROFILE.vehicle_electrical_specification = "valid" 2327 2328 _GENERATED_PROFILE.vehicle_electrical_power = vehicle_electrical_power 2329 logger.write_result_to_html_report("Test Vehicle Electrical Power #5 command was successful") 2330 2331 def test_comparison(self) -> None: 2332 """ 2333 | Description | Compare if Vehicle Electrical Power #5 values matched | 2334 | :------------------- | :--------------------------------------------------------------- | 2335 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 2336 | Instructions | 1. Check if Vehicle Electrical Power #5 matched in profiles | 2337 | Pass / Fail Criteria | Pass if Vehicle Electrical Power #5 values match | 2338 | Estimated Duration | 1 second | 2339 """ 2340 2341 failed_test = False 2342 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_power") or not hasattr( 2343 _COMPARISON_PROFILE, "vehicle_electrical_power" 2344 ): 2345 message = "Nothing to compare. The profile test may have failed or been skipped" 2346 logger.write_result_to_html_report(message) 2347 pytest.skip(message) 2348 2349 if isinstance(_GENERATED_PROFILE.vehicle_electrical_power, str) or isinstance( 2350 _COMPARISON_PROFILE.vehicle_electrical_power, str 2351 ): 2352 if not isinstance( 2353 _GENERATED_PROFILE.vehicle_electrical_power, type(_COMPARISON_PROFILE.vehicle_electrical_power) 2354 ): 2355 message = "Unable to compare profiles, Vehicle Electrical Power #5 types did not match" 2356 logger.write_failure_to_html_report(message) 2357 pytest.fail(message) 2358 2359 comparison = cmp( 2360 _GENERATED_PROFILE.vehicle_electrical_power, "==", _COMPARISON_PROFILE.vehicle_electrical_power 2361 ) 2362 2363 if _GENERATED_PROFILE.vehicle_electrical_power != _COMPARISON_PROFILE.vehicle_electrical_power: 2364 message = f"Vehicle Electrical Power #5 did not match: {comparison}" 2365 logger.write_failure_to_html_report(message) 2366 pytest.fail(message) 2367 2368 message = f"Vehicle Electrical Power #5 matched: {comparison}" 2369 logger.write_result_to_html_report(message) 2370 else: 2371 2372 failed_categories = [] 2373 percent_closeness = 0.05 2374 2375 for key, value in _GENERATED_PROFILE.vehicle_electrical_power.items(): 2376 key_text = key.replace("_", " ").title() 2377 key_text = key_text.replace("Sli", "SLI") 2378 diff = abs(value - _COMPARISON_PROFILE.vehicle_electrical_power[key]) 2379 average = (value + _COMPARISON_PROFILE.vehicle_electrical_power[key]) / 2 2380 percentage = round(diff / average, 4) 2381 2382 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 2383 message = f"{key_text}: {comparison}" 2384 2385 if not math.isclose( 2386 value, 2387 _COMPARISON_PROFILE.vehicle_electrical_power[key], 2388 rel_tol=percent_closeness, 2389 ): 2390 logger.write_warning_to_html_report(message) 2391 failed_categories.append(key_text) 2392 else: 2393 logger.write_result_to_html_report(message) 2394 2395 fail_length = len(failed_categories) 2396 2397 if fail_length > 0: 2398 categories = "data values" if fail_length > 1 else "data value" 2399 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2400 logger.write_failure_to_html_report(message) 2401 failed_test = True 2402 else: 2403 logger.write_result_to_html_report( 2404 f"Vehicle Electrical Power #5 values between profiles were within {percent_closeness * 100}%" 2405 ) 2406 2407 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_specification") or not hasattr( 2408 _COMPARISON_PROFILE, "vehicle_electrical_specification" 2409 ): 2410 logger.write_warning_to_html_report("Missing specification information") 2411 else: 2412 comparison = cmp( 2413 _GENERATED_PROFILE.vehicle_electrical_specification, 2414 "==", 2415 _COMPARISON_PROFILE.vehicle_electrical_specification, 2416 ) 2417 if ( 2418 _GENERATED_PROFILE.vehicle_electrical_specification 2419 != _COMPARISON_PROFILE.vehicle_electrical_specification 2420 ): 2421 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2422 failed_test = True 2423 else: 2424 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2425 2426 if failed_test: 2427 message = "Comparisons did not match in profiles" 2428 logger.write_failure_to_html_report(message) 2429 pytest.fail(message)
Test Vehicle Electrical Power command is J1939 Compliant
2242 @pytest.mark.profile 2243 def test_profile(self) -> None: 2244 """ 2245 | Description | Test Vehicle Electrical Power #5 (VEP5) command | 2246 | :------------------- | :--------------------------------------------------------------- | 2247 | GitHub Issue | turnaroundfactor/BMS-HW-Test#389 | 2248 | Instructions | 1. Send Vehicle Electrical Power #5 command </br>\ 2249 2. Check values in data </br>\ 2250 3. Log response | 2251 | Estimated Duration | 21 seconds | 2252 """ 2253 2254 with CANBus(BATTERY_CHANNEL) as bus: 2255 vehicle_electrical_power_request = CANFrame( 2256 destination_address=_GENERATED_PROFILE.address, 2257 pgn=PGN["Request"], 2258 data=[PGN["Vehicle Electrical Power #5"].id], 2259 ) 2260 if vehicle_electrical_power_response := bus.process_call(vehicle_electrical_power_request): 2261 if vehicle_electrical_power_response.pgn.id != PGN["Vehicle Electrical Power #5", [32]].id: 2262 if vehicle_electrical_power_response.pgn.id == 59392: 2263 message = ( 2264 f"Received PGN {vehicle_electrical_power_response.pgn.id} " 2265 f"({spn_types.acknowledgement(vehicle_electrical_power_response.data[0])}), " 2266 f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} ' 2267 ) 2268 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.NACK.name 2269 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2270 else: 2271 message = ( 2272 f"Received PGN {vehicle_electrical_power_response.pgn.id} " 2273 f"{vehicle_electrical_power_response.pgn.short_name}, " 2274 f'not PGN {PGN["Vehicle Electrical Power #5", [32]].id} ' 2275 ) 2276 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.WRONG_PGN.name 2277 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2278 logger.write_warning_to_html_report(message) 2279 return 2280 else: 2281 message = ( 2282 f"Battery did not respond to PGN {PGN['Vehicle Electrical Power #5', [32]].id} " 2283 f"Vehicle Electrical Power #5 request" 2284 ) 2285 _GENERATED_PROFILE.vehicle_electrical_power = BadStates.NO_RESPONSE.name 2286 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2287 logger.write_warning_to_html_report(message) 2288 return 2289 2290 vep5_data = vehicle_electrical_power_response.data 2291 vep5_pgn = PGN["Vehicle Electrical Power #5"].data_field 2292 2293 logger.write_result_to_html_report("<span style='font-weight: bold'>Vehicle Electrical Power #5</span>") 2294 2295 for spn, elem in zip(vep5_pgn, vep5_data): 2296 if spn.name in ["Reserved", "SLI Battery Pack State of Charge"]: 2297 continue 2298 2299 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 2300 logger.write_result_to_html_report(f"{spn.name}: {elem} {description}") 2301 high_value = 1.0 2302 2303 if spn.name == "SLI Battery Pack Capacity": 2304 high_value = 64255 2305 2306 if spn.name == "SLI Battery Pack Health": 2307 high_value = 125 2308 2309 if spn.name == "SLI Cranking Predicted Minimum Battery Voltage": 2310 high_value = 50 2311 2312 if not 0 <= elem <= high_value: 2313 logger.write_warning_to_html_report( 2314 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 2315 f"{cmp(elem, '<=', high_value, description)}" 2316 ) 2317 _GENERATED_PROFILE.vehicle_electrical_specification = "invalid" 2318 2319 vehicle_electrical_power = { 2320 "sli_battery_pack_capacity": vep5_data[1], 2321 "sli_battery_pack_health": vep5_data[2], 2322 "sli_cranking_predicted_minimum_battery_voltage": vep5_data[3], 2323 } 2324 2325 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_specification"): 2326 _GENERATED_PROFILE.vehicle_electrical_specification = "valid" 2327 2328 _GENERATED_PROFILE.vehicle_electrical_power = vehicle_electrical_power 2329 logger.write_result_to_html_report("Test Vehicle Electrical Power #5 command was successful")
| Description | Test Vehicle Electrical Power #5 (VEP5) command |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#389 |
| Instructions | 1. Send Vehicle Electrical Power #5 command 2. Check values in data 3. Log response |
| Estimated Duration | 21 seconds |
2331 def test_comparison(self) -> None: 2332 """ 2333 | Description | Compare if Vehicle Electrical Power #5 values matched | 2334 | :------------------- | :--------------------------------------------------------------- | 2335 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 2336 | Instructions | 1. Check if Vehicle Electrical Power #5 matched in profiles | 2337 | Pass / Fail Criteria | Pass if Vehicle Electrical Power #5 values match | 2338 | Estimated Duration | 1 second | 2339 """ 2340 2341 failed_test = False 2342 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_power") or not hasattr( 2343 _COMPARISON_PROFILE, "vehicle_electrical_power" 2344 ): 2345 message = "Nothing to compare. The profile test may have failed or been skipped" 2346 logger.write_result_to_html_report(message) 2347 pytest.skip(message) 2348 2349 if isinstance(_GENERATED_PROFILE.vehicle_electrical_power, str) or isinstance( 2350 _COMPARISON_PROFILE.vehicle_electrical_power, str 2351 ): 2352 if not isinstance( 2353 _GENERATED_PROFILE.vehicle_electrical_power, type(_COMPARISON_PROFILE.vehicle_electrical_power) 2354 ): 2355 message = "Unable to compare profiles, Vehicle Electrical Power #5 types did not match" 2356 logger.write_failure_to_html_report(message) 2357 pytest.fail(message) 2358 2359 comparison = cmp( 2360 _GENERATED_PROFILE.vehicle_electrical_power, "==", _COMPARISON_PROFILE.vehicle_electrical_power 2361 ) 2362 2363 if _GENERATED_PROFILE.vehicle_electrical_power != _COMPARISON_PROFILE.vehicle_electrical_power: 2364 message = f"Vehicle Electrical Power #5 did not match: {comparison}" 2365 logger.write_failure_to_html_report(message) 2366 pytest.fail(message) 2367 2368 message = f"Vehicle Electrical Power #5 matched: {comparison}" 2369 logger.write_result_to_html_report(message) 2370 else: 2371 2372 failed_categories = [] 2373 percent_closeness = 0.05 2374 2375 for key, value in _GENERATED_PROFILE.vehicle_electrical_power.items(): 2376 key_text = key.replace("_", " ").title() 2377 key_text = key_text.replace("Sli", "SLI") 2378 diff = abs(value - _COMPARISON_PROFILE.vehicle_electrical_power[key]) 2379 average = (value + _COMPARISON_PROFILE.vehicle_electrical_power[key]) / 2 2380 percentage = round(diff / average, 4) 2381 2382 comparison = cmp(percentage * 100, "<=", percent_closeness * 100, "%") 2383 message = f"{key_text}: {comparison}" 2384 2385 if not math.isclose( 2386 value, 2387 _COMPARISON_PROFILE.vehicle_electrical_power[key], 2388 rel_tol=percent_closeness, 2389 ): 2390 logger.write_warning_to_html_report(message) 2391 failed_categories.append(key_text) 2392 else: 2393 logger.write_result_to_html_report(message) 2394 2395 fail_length = len(failed_categories) 2396 2397 if fail_length > 0: 2398 categories = "data values" if fail_length > 1 else "data value" 2399 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2400 logger.write_failure_to_html_report(message) 2401 failed_test = True 2402 else: 2403 logger.write_result_to_html_report( 2404 f"Vehicle Electrical Power #5 values between profiles were within {percent_closeness * 100}%" 2405 ) 2406 2407 if not hasattr(_GENERATED_PROFILE, "vehicle_electrical_specification") or not hasattr( 2408 _COMPARISON_PROFILE, "vehicle_electrical_specification" 2409 ): 2410 logger.write_warning_to_html_report("Missing specification information") 2411 else: 2412 comparison = cmp( 2413 _GENERATED_PROFILE.vehicle_electrical_specification, 2414 "==", 2415 _COMPARISON_PROFILE.vehicle_electrical_specification, 2416 ) 2417 if ( 2418 _GENERATED_PROFILE.vehicle_electrical_specification 2419 != _COMPARISON_PROFILE.vehicle_electrical_specification 2420 ): 2421 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2422 failed_test = True 2423 else: 2424 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2425 2426 if failed_test: 2427 message = "Comparisons did not match in profiles" 2428 logger.write_failure_to_html_report(message) 2429 pytest.fail(message)
| Description | Compare if Vehicle Electrical Power #5 values matched |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if Vehicle Electrical Power #5 matched in profiles |
| Pass / Fail Criteria | Pass if Vehicle Electrical Power #5 values match |
| Estimated Duration | 1 second |
2432class TestManufacturerCommands: 2433 """Test Manufacturer Commands are compliant with specs""" 2434 2435 def saft_commands_test(self): 2436 """Tests SAFT Manufacturer Commands""" 2437 with CANBus(BATTERY_CHANNEL) as bus: 2438 manufactured_command_request = CANFrame( 2439 destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[0] 2440 ) 2441 invalid_response = [] 2442 for address in itertools.chain( 2443 [0xFFD2], range(0xFFD4, 0xFFD9), range(0xFFDC, 0xFFDF), range(0xFFE0, 0xFFE2), [0xFFE4] 2444 ): 2445 manufactured_command_request.data = [address] 2446 default_pgn = PGN[address] 2447 pgn_name = default_pgn.name 2448 logger.write_result_to_html_report( 2449 f"<span style='font-weight: bold'>PGN {address} ({pgn_name})---</span>" 2450 ) 2451 2452 if response_frame := bus.process_call(manufactured_command_request): 2453 if not response_frame.pgn.id == default_pgn.id: 2454 2455 if response_frame.pgn.id == PGN["ACKM", [32]].id: 2456 message = ( 2457 f"Expected {address} ({pgn_name}): Received PGN {response_frame.pgn.id} " 2458 f"({spn_types.acknowledgement(response_frame.data[0])}) " 2459 ) 2460 logger.write_warning_to_html_report(message) 2461 else: 2462 logger.write_warning_to_html_report( 2463 f"Expected PGN {address} ({pgn_name}), but received " 2464 f"{response_frame.pgn.id} ({response_frame.pgn.name}). " 2465 f"Unable to complete check for command" 2466 ) 2467 invalid_response.append(f"PGN {address} ({pgn_name})") 2468 continue 2469 else: 2470 message = f"Did not receive response from PGN {address} {pgn_name}" 2471 logger.write_warning_to_html_report(message) 2472 invalid_response.append(f"PGN {address} ({pgn_name})") 2473 2474 continue 2475 2476 if response_frame.priority != default_pgn.default_priority: 2477 message = ( 2478 f"Expected priority level of {default_pgn.default_priority}" 2479 f" but got priority level {response_frame.priority} for PGN {address}, {pgn_name}" 2480 ) 2481 invalid_response.append(f"PGN {address} {pgn_name}") 2482 logger.write_warning_to_html_report(message) 2483 2484 if len(response_frame.packed_data) != 8: 2485 message = ( 2486 f"Unexpected data length for PGN {address}, {pgn_name}. Expected length of 8, " 2487 f"received {len(response_frame.packed_data)}" 2488 ) 2489 logger.write_warning_to_html_report(message) 2490 2491 not_passed_elem = [] 2492 for spn, elem in zip(default_pgn.data_field, response_frame.data): 2493 low_range = 0 2494 high_range = 3 2495 spn_name = spn.name 2496 if spn.name == "Reserved": 2497 continue 2498 2499 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 2500 2501 logger.write_result_to_html_report(f"{spn_name}: {elem}{description}") 2502 2503 if pgn_name == "Battery ECU Status": 2504 if spn_name in ("Battery Mode", "FET Array State", "SOC Mode", "Heat Reason"): 2505 high_range = 7 2506 elif spn_name == "Long-Term Fault Log Status": 2507 high_range = 15 2508 elif spn_name == "Software Part Number": 2509 high_range = 64255 2510 2511 if pgn_name in ("Battery Cell Status 1", "Battery Cell Status 2"): 2512 high_range = 6.4255 2513 2514 if pgn_name == "Battery Performance": 2515 if spn_name == "Battery Current": 2516 low_range = -82000.00 2517 high_range = 82495.35 2518 if spn_name == "Internal State of Health": 2519 low_range = -204.800 2520 high_range = 204.775 2521 2522 if pgn_name == "Battery Temperatures": 2523 if spn_name == "MCU Temperature": 2524 low_range = -40 2525 high_range = 210 2526 else: 2527 low_range = -50 2528 high_range = 200 2529 2530 if pgn_name == "Battery Balancing Circuit Info": 2531 if spn.name == "Cell Voltage Difference": 2532 high_range = 6.4255 2533 if spn_name == "Cell Voltage Sum": 2534 high_range = 104.8576 2535 2536 if pgn_name in ("Battery Cell Upper SOC", "Battery Cell Lower SOC"): 2537 low_range = -10 2538 high_range = 115 2539 2540 if pgn_name == "Battery Function Status": 2541 if spn_name == "Heater Set Point": 2542 low_range = -50 2543 high_range = 25 2544 if spn_name == "Storage Delay Time Limit": 2545 high_range = 65535 2546 if spn_name == "Last Storage Duration (Minutes)": 2547 high_range = 59 2548 if spn_name == "Last Storage Duration (Hours)": 2549 high_range = 23 2550 if spn_name == "Last Storage Duration (Days)": 2551 high_range = 31 2552 if spn_name == "Last Storage Duration (Months)": 2553 high_range = 255 2554 if spn_name == "Effective Reset Time": 2555 high_range = 60 2556 2557 if not low_range <= elem <= high_range: 2558 logger.write_warning_to_html_report( 2559 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 2560 f"{cmp(elem, '<=', high_range, description)}" 2561 ) 2562 not_passed_elem.append(spn_name) 2563 2564 if len(not_passed_elem) == 0: 2565 message = f"✅ All data fields in PGN {default_pgn.id} ({pgn_name}) met requirements" 2566 logger.write_result_to_html_report(message) 2567 2568 if len(invalid_response) > 0: 2569 message = ( 2570 f"{len(invalid_response)} SAFT Manufacturer Command{'s' if len(invalid_response) > 1 else ''} " 2571 f"failed: {', '.join(invalid_response)}" 2572 ) 2573 logger.write_warning_to_html_report(message) 2574 else: 2575 logger.write_result_to_html_report("All SAFT Manufacturer Commands passed") 2576 2577 @pytest.mark.profile 2578 def test_profile(self) -> None: 2579 """ 2580 | Description | Fingerprint Manufacturer Commands | 2581 | :------------------- | :--------------------------------------------------------------- | 2582 | GitHub Issue | turnaroundfactor/BMS-HW-Test#388 | 2583 | Instructions | 1. Send request for manufacturer command </br>\ 2584 2. Check values in data </br>\ 2585 3. Log response | 2586 | Estimated Duration | 22 seconds | 2587 """ 2588 2589 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.SAFT: 2590 self.saft_commands_test() 2591 else: 2592 message = "No known manufacturer commands to test" 2593 logger.write_result_to_html_report(message) 2594 pytest.skip(message)
Test Manufacturer Commands are compliant with specs
2435 def saft_commands_test(self): 2436 """Tests SAFT Manufacturer Commands""" 2437 with CANBus(BATTERY_CHANNEL) as bus: 2438 manufactured_command_request = CANFrame( 2439 destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[0] 2440 ) 2441 invalid_response = [] 2442 for address in itertools.chain( 2443 [0xFFD2], range(0xFFD4, 0xFFD9), range(0xFFDC, 0xFFDF), range(0xFFE0, 0xFFE2), [0xFFE4] 2444 ): 2445 manufactured_command_request.data = [address] 2446 default_pgn = PGN[address] 2447 pgn_name = default_pgn.name 2448 logger.write_result_to_html_report( 2449 f"<span style='font-weight: bold'>PGN {address} ({pgn_name})---</span>" 2450 ) 2451 2452 if response_frame := bus.process_call(manufactured_command_request): 2453 if not response_frame.pgn.id == default_pgn.id: 2454 2455 if response_frame.pgn.id == PGN["ACKM", [32]].id: 2456 message = ( 2457 f"Expected {address} ({pgn_name}): Received PGN {response_frame.pgn.id} " 2458 f"({spn_types.acknowledgement(response_frame.data[0])}) " 2459 ) 2460 logger.write_warning_to_html_report(message) 2461 else: 2462 logger.write_warning_to_html_report( 2463 f"Expected PGN {address} ({pgn_name}), but received " 2464 f"{response_frame.pgn.id} ({response_frame.pgn.name}). " 2465 f"Unable to complete check for command" 2466 ) 2467 invalid_response.append(f"PGN {address} ({pgn_name})") 2468 continue 2469 else: 2470 message = f"Did not receive response from PGN {address} {pgn_name}" 2471 logger.write_warning_to_html_report(message) 2472 invalid_response.append(f"PGN {address} ({pgn_name})") 2473 2474 continue 2475 2476 if response_frame.priority != default_pgn.default_priority: 2477 message = ( 2478 f"Expected priority level of {default_pgn.default_priority}" 2479 f" but got priority level {response_frame.priority} for PGN {address}, {pgn_name}" 2480 ) 2481 invalid_response.append(f"PGN {address} {pgn_name}") 2482 logger.write_warning_to_html_report(message) 2483 2484 if len(response_frame.packed_data) != 8: 2485 message = ( 2486 f"Unexpected data length for PGN {address}, {pgn_name}. Expected length of 8, " 2487 f"received {len(response_frame.packed_data)}" 2488 ) 2489 logger.write_warning_to_html_report(message) 2490 2491 not_passed_elem = [] 2492 for spn, elem in zip(default_pgn.data_field, response_frame.data): 2493 low_range = 0 2494 high_range = 3 2495 spn_name = spn.name 2496 if spn.name == "Reserved": 2497 continue 2498 2499 description = f"({desc})" if (desc := spn.data_type(elem)) else "" 2500 2501 logger.write_result_to_html_report(f"{spn_name}: {elem}{description}") 2502 2503 if pgn_name == "Battery ECU Status": 2504 if spn_name in ("Battery Mode", "FET Array State", "SOC Mode", "Heat Reason"): 2505 high_range = 7 2506 elif spn_name == "Long-Term Fault Log Status": 2507 high_range = 15 2508 elif spn_name == "Software Part Number": 2509 high_range = 64255 2510 2511 if pgn_name in ("Battery Cell Status 1", "Battery Cell Status 2"): 2512 high_range = 6.4255 2513 2514 if pgn_name == "Battery Performance": 2515 if spn_name == "Battery Current": 2516 low_range = -82000.00 2517 high_range = 82495.35 2518 if spn_name == "Internal State of Health": 2519 low_range = -204.800 2520 high_range = 204.775 2521 2522 if pgn_name == "Battery Temperatures": 2523 if spn_name == "MCU Temperature": 2524 low_range = -40 2525 high_range = 210 2526 else: 2527 low_range = -50 2528 high_range = 200 2529 2530 if pgn_name == "Battery Balancing Circuit Info": 2531 if spn.name == "Cell Voltage Difference": 2532 high_range = 6.4255 2533 if spn_name == "Cell Voltage Sum": 2534 high_range = 104.8576 2535 2536 if pgn_name in ("Battery Cell Upper SOC", "Battery Cell Lower SOC"): 2537 low_range = -10 2538 high_range = 115 2539 2540 if pgn_name == "Battery Function Status": 2541 if spn_name == "Heater Set Point": 2542 low_range = -50 2543 high_range = 25 2544 if spn_name == "Storage Delay Time Limit": 2545 high_range = 65535 2546 if spn_name == "Last Storage Duration (Minutes)": 2547 high_range = 59 2548 if spn_name == "Last Storage Duration (Hours)": 2549 high_range = 23 2550 if spn_name == "Last Storage Duration (Days)": 2551 high_range = 31 2552 if spn_name == "Last Storage Duration (Months)": 2553 high_range = 255 2554 if spn_name == "Effective Reset Time": 2555 high_range = 60 2556 2557 if not low_range <= elem <= high_range: 2558 logger.write_warning_to_html_report( 2559 f"{spn.name}: {cmp(elem, '>=', 0, description)} and " 2560 f"{cmp(elem, '<=', high_range, description)}" 2561 ) 2562 not_passed_elem.append(spn_name) 2563 2564 if len(not_passed_elem) == 0: 2565 message = f"✅ All data fields in PGN {default_pgn.id} ({pgn_name}) met requirements" 2566 logger.write_result_to_html_report(message) 2567 2568 if len(invalid_response) > 0: 2569 message = ( 2570 f"{len(invalid_response)} SAFT Manufacturer Command{'s' if len(invalid_response) > 1 else ''} " 2571 f"failed: {', '.join(invalid_response)}" 2572 ) 2573 logger.write_warning_to_html_report(message) 2574 else: 2575 logger.write_result_to_html_report("All SAFT Manufacturer Commands passed")
Tests SAFT Manufacturer Commands
2577 @pytest.mark.profile 2578 def test_profile(self) -> None: 2579 """ 2580 | Description | Fingerprint Manufacturer Commands | 2581 | :------------------- | :--------------------------------------------------------------- | 2582 | GitHub Issue | turnaroundfactor/BMS-HW-Test#388 | 2583 | Instructions | 1. Send request for manufacturer command </br>\ 2584 2. Check values in data </br>\ 2585 3. Log response | 2586 | Estimated Duration | 22 seconds | 2587 """ 2588 2589 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.SAFT: 2590 self.saft_commands_test() 2591 else: 2592 message = "No known manufacturer commands to test" 2593 logger.write_result_to_html_report(message) 2594 pytest.skip(message)
| Description | Fingerprint Manufacturer Commands |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#388 |
| Instructions | 1. Send request for manufacturer command 2. Check values in data 3. Log response |
| Estimated Duration | 22 seconds |
2720class TestECUInformation: 2721 """Gets information about the physical ECU and its hardware""" 2722 2723 @staticmethod 2724 def bytes_to_ascii(bs: list[float]) -> list[str]: 2725 """Converts bytes to ASCII string""" 2726 s: str = "" 2727 for b in bs: 2728 h = re.sub(r"^[^0-9a-fA-F]+$", "", f"{b:x}") 2729 try: 2730 ba = bytearray.fromhex(h)[::-1] 2731 s += ba.decode("utf-8", "ignore") 2732 s = re.sub(r"[^\x20-\x7E]", "", s) 2733 except ValueError: 2734 # NOTE: This will ignore any invalid packets (from BrenTronics) 2735 logger.write_warning_to_report(f"Skipping invalid hex: {b:x}") 2736 return list(filter(None, s.split("*"))) 2737 2738 @pytest.mark.profile 2739 def test_ecu_information(self) -> None: 2740 """ 2741 | Description | Get information from ECUID response | 2742 | :------------------- | :--------------------------------------------------------------- | 2743 | GitHub Issue | turnaroundfactor/BMS-HW-Test#395 | 2744 | Instructions | 1. Request ECUID data </br>\ 2745 2. Log returned values | 2746 | Estimated Duration | 30 seconds | 2747 """ 2748 2749 info = [] 2750 with CANBus(BATTERY_CHANNEL) as bus: 2751 ecu_request = CANFrame(pgn=PGN["Request"], data=[PGN["ECUID"].id]) 2752 if tp_cm_frame := bus.process_call(ecu_request): 2753 if tp_cm_frame is not None: 2754 if tp_cm_frame.pgn.id == PGN["TP.CM", [32]].id: 2755 data = [] 2756 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 2757 if data_frame := bus.process_call(cts_request): 2758 if data_frame is not None: 2759 if data_frame.pgn.id == PGN["TP.DT"].id: 2760 packet_count = int(tp_cm_frame.data[2]) 2761 # NOTE: This will ignore the invalid header packet from BrenTronics 2762 if _GENERATED_PROFILE.manufacturer_code != ManufacturerID.BRENTRONICS: 2763 data.append(data_frame.data[1]) 2764 packet_count -= 1 2765 for _ in range(packet_count): 2766 data_message = bus.read_input() 2767 if data_message is not None: 2768 frame = CANFrame.decode(data_message.arbitration_id, data_message.data) 2769 if frame.pgn.id == PGN["TP.DT"].id: 2770 data.append(frame.data[1]) 2771 else: 2772 Errors.unexpected_packet("TP.DT", frame) 2773 break 2774 else: 2775 Errors.no_packet("TP.DT") 2776 break 2777 info = self.bytes_to_ascii(data) 2778 eom_request = CANFrame( 2779 pgn=PGN["TP.CM"], 2780 data=[17, tp_cm_frame.data[1], packet_count, 0xFF, tp_cm_frame.data[-1]], 2781 ) 2782 if eom_frame := bus.process_call(eom_request): 2783 if eom_frame is not None: 2784 if eom_frame.pgn.id == PGN["DM15"].id: 2785 if eom_frame.data[2] == 4: # Operation Completed 2786 logger.write_info_to_report("ECUID data transfer successful") 2787 else: 2788 logger.write_warning_to_html_report("Unsuccessful EOM response") 2789 else: 2790 Errors.unexpected_packet("DM15", eom_frame) 2791 else: 2792 Errors.no_packet("DM15") 2793 else: 2794 # timeout 2795 logger.write_warning_to_report("No response after sending EOM (DM15)") 2796 else: 2797 Errors.unexpected_packet("TP.DT", data_frame) 2798 else: 2799 Errors.no_packet("TP.DT") 2800 else: 2801 message = f"Did not receive response from PGN {PGN['TP.CM', [32]].id}" 2802 logger.write_warning_to_html_report(message) 2803 _GENERATED_PROFILE.ecu = BadStates.NO_RESPONSE.name 2804 _GENERATED_PROFILE.ecu_specification = "invalid" 2805 else: 2806 Errors.unexpected_packet("TP.CM", tp_cm_frame) 2807 else: 2808 Errors.no_packet("TP.CM") 2809 else: 2810 message = f"Did not receive response from PGN {PGN['ECUID', [32]].id}" 2811 logger.write_warning_to_html_report(message) 2812 _GENERATED_PROFILE.ecu = BadStates.NO_RESPONSE.name 2813 _GENERATED_PROFILE.ecu_specification = "invalid" 2814 2815 if len(info) > 0: 2816 _GENERATED_PROFILE.ecu = { 2817 "part_number": info[0], 2818 "serial_number": info[1], 2819 "location_name": info[2], 2820 "manufacturer": info[3], 2821 "classification": info[4], 2822 } 2823 logger.write_result_to_html_report("<span style='font-weight: bold'>ECUID Information </span>") 2824 for key, value in _GENERATED_PROFILE.ecu.items(): 2825 logger.write_result_to_html_report(f"{key.strip().replace('_', ' ').title()}: {value}") 2826 _GENERATED_PROFILE.ecu_specification = "valid" 2827 2828 else: 2829 _GENERATED_PROFILE.ecu = BadStates.INVALID_RESPONSE.name 2830 logger.write_warning_to_html_report("Could not get ECU information") 2831 _GENERATED_PROFILE.ecu_specification = "invalid" 2832 2833 def test_comparison(self) -> None: 2834 """ 2835 | Description | Compare ECU Information | 2836 | :------------------- | :--------------------------------------------------------------- | 2837 | GitHub Issue | turnaroundfactor/BMS-HW-Test#395 | 2838 | Instructions | 1. Confirm identity numbers are unique for each battery | 2839 | Pass / Fail Criteria | Pass if ECU values match / serial number is unique | 2840 | Estimated Duration | 1 second | 2841 """ 2842 2843 failed_test = False 2844 if not hasattr(_GENERATED_PROFILE, "ecu") or not hasattr(_COMPARISON_PROFILE, "ecu"): 2845 logger.write_result_to_html_report("Nothing to compare. The profile test may have failed or been skipped") 2846 pytest.skip("Nothing to compare. The profile test may have failed or been skipped") 2847 2848 if isinstance(_GENERATED_PROFILE.ecu, str) or isinstance(_COMPARISON_PROFILE.ecu, str): 2849 if not isinstance(_GENERATED_PROFILE.ecu, type(_COMPARISON_PROFILE.ecu)): 2850 message = "Unable to compare profiles, ECU types did not match" 2851 logger.write_failure_to_html_report(message) 2852 pytest.fail(message) 2853 2854 comparison = cmp(_GENERATED_PROFILE.ecu, "==", _COMPARISON_PROFILE.ecu) 2855 2856 if _GENERATED_PROFILE.ecu != _COMPARISON_PROFILE.ecu: 2857 message = f"ECU did not match: {comparison}" 2858 logger.write_failure_to_html_report(message) 2859 pytest.fail(message) 2860 2861 message = f"ECU matched: {comparison}" 2862 logger.write_result_to_html_report(message) 2863 else: 2864 2865 failed_categories = [] 2866 2867 for key, value in _GENERATED_PROFILE.ecu.items(): 2868 key_text = key.replace("_", " ").title() 2869 2870 comparison = cmp(value, "==", _COMPARISON_PROFILE.ecu[key]) 2871 2872 if key == "serial_number": 2873 if value == _COMPARISON_PROFILE.ecu[key]: 2874 message = f"{key_text} matched: {comparison}" 2875 logger.write_warning_to_html_report(message) 2876 else: 2877 message = f"{key_text}: {comparison}" 2878 logger.write_result_to_html_report(message) 2879 2880 elif value != _COMPARISON_PROFILE.ecu[key]: 2881 message = f"{key_text} did not match: {comparison}" 2882 logger.write_warning_to_html_report(message) 2883 failed_categories.append(key_text) 2884 else: 2885 message = f"{key_text}: {comparison}" 2886 logger.write_result_to_html_report(message) 2887 2888 fail_length = len(failed_categories) 2889 2890 if fail_length > 0: 2891 categories = "data values" if fail_length > 1 else "data value" 2892 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2893 logger.write_failure_to_html_report(message) 2894 failed_test = True 2895 2896 if not hasattr(_GENERATED_PROFILE, "ecu_specification") or not hasattr( 2897 _COMPARISON_PROFILE, "ecu_specification" 2898 ): 2899 logger.write_warning_to_html_report("Missing specification information") 2900 else: 2901 comparison = cmp( 2902 _GENERATED_PROFILE.ecu_specification, 2903 "==", 2904 _COMPARISON_PROFILE.ecu_specification, 2905 ) 2906 if _GENERATED_PROFILE.ecu_specification != _COMPARISON_PROFILE.ecu_specification: 2907 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2908 failed_test = True 2909 else: 2910 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2911 2912 if failed_test: 2913 message = "Comparisons did not meet expectations" 2914 logger.write_failure_to_html_report(message) 2915 pytest.fail(message) 2916 else: 2917 logger.write_result_to_html_report("ECUID profile comparisons met requirements")
Gets information about the physical ECU and its hardware
2723 @staticmethod 2724 def bytes_to_ascii(bs: list[float]) -> list[str]: 2725 """Converts bytes to ASCII string""" 2726 s: str = "" 2727 for b in bs: 2728 h = re.sub(r"^[^0-9a-fA-F]+$", "", f"{b:x}") 2729 try: 2730 ba = bytearray.fromhex(h)[::-1] 2731 s += ba.decode("utf-8", "ignore") 2732 s = re.sub(r"[^\x20-\x7E]", "", s) 2733 except ValueError: 2734 # NOTE: This will ignore any invalid packets (from BrenTronics) 2735 logger.write_warning_to_report(f"Skipping invalid hex: {b:x}") 2736 return list(filter(None, s.split("*")))
Converts bytes to ASCII string
2738 @pytest.mark.profile 2739 def test_ecu_information(self) -> None: 2740 """ 2741 | Description | Get information from ECUID response | 2742 | :------------------- | :--------------------------------------------------------------- | 2743 | GitHub Issue | turnaroundfactor/BMS-HW-Test#395 | 2744 | Instructions | 1. Request ECUID data </br>\ 2745 2. Log returned values | 2746 | Estimated Duration | 30 seconds | 2747 """ 2748 2749 info = [] 2750 with CANBus(BATTERY_CHANNEL) as bus: 2751 ecu_request = CANFrame(pgn=PGN["Request"], data=[PGN["ECUID"].id]) 2752 if tp_cm_frame := bus.process_call(ecu_request): 2753 if tp_cm_frame is not None: 2754 if tp_cm_frame.pgn.id == PGN["TP.CM", [32]].id: 2755 data = [] 2756 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 2757 if data_frame := bus.process_call(cts_request): 2758 if data_frame is not None: 2759 if data_frame.pgn.id == PGN["TP.DT"].id: 2760 packet_count = int(tp_cm_frame.data[2]) 2761 # NOTE: This will ignore the invalid header packet from BrenTronics 2762 if _GENERATED_PROFILE.manufacturer_code != ManufacturerID.BRENTRONICS: 2763 data.append(data_frame.data[1]) 2764 packet_count -= 1 2765 for _ in range(packet_count): 2766 data_message = bus.read_input() 2767 if data_message is not None: 2768 frame = CANFrame.decode(data_message.arbitration_id, data_message.data) 2769 if frame.pgn.id == PGN["TP.DT"].id: 2770 data.append(frame.data[1]) 2771 else: 2772 Errors.unexpected_packet("TP.DT", frame) 2773 break 2774 else: 2775 Errors.no_packet("TP.DT") 2776 break 2777 info = self.bytes_to_ascii(data) 2778 eom_request = CANFrame( 2779 pgn=PGN["TP.CM"], 2780 data=[17, tp_cm_frame.data[1], packet_count, 0xFF, tp_cm_frame.data[-1]], 2781 ) 2782 if eom_frame := bus.process_call(eom_request): 2783 if eom_frame is not None: 2784 if eom_frame.pgn.id == PGN["DM15"].id: 2785 if eom_frame.data[2] == 4: # Operation Completed 2786 logger.write_info_to_report("ECUID data transfer successful") 2787 else: 2788 logger.write_warning_to_html_report("Unsuccessful EOM response") 2789 else: 2790 Errors.unexpected_packet("DM15", eom_frame) 2791 else: 2792 Errors.no_packet("DM15") 2793 else: 2794 # timeout 2795 logger.write_warning_to_report("No response after sending EOM (DM15)") 2796 else: 2797 Errors.unexpected_packet("TP.DT", data_frame) 2798 else: 2799 Errors.no_packet("TP.DT") 2800 else: 2801 message = f"Did not receive response from PGN {PGN['TP.CM', [32]].id}" 2802 logger.write_warning_to_html_report(message) 2803 _GENERATED_PROFILE.ecu = BadStates.NO_RESPONSE.name 2804 _GENERATED_PROFILE.ecu_specification = "invalid" 2805 else: 2806 Errors.unexpected_packet("TP.CM", tp_cm_frame) 2807 else: 2808 Errors.no_packet("TP.CM") 2809 else: 2810 message = f"Did not receive response from PGN {PGN['ECUID', [32]].id}" 2811 logger.write_warning_to_html_report(message) 2812 _GENERATED_PROFILE.ecu = BadStates.NO_RESPONSE.name 2813 _GENERATED_PROFILE.ecu_specification = "invalid" 2814 2815 if len(info) > 0: 2816 _GENERATED_PROFILE.ecu = { 2817 "part_number": info[0], 2818 "serial_number": info[1], 2819 "location_name": info[2], 2820 "manufacturer": info[3], 2821 "classification": info[4], 2822 } 2823 logger.write_result_to_html_report("<span style='font-weight: bold'>ECUID Information </span>") 2824 for key, value in _GENERATED_PROFILE.ecu.items(): 2825 logger.write_result_to_html_report(f"{key.strip().replace('_', ' ').title()}: {value}") 2826 _GENERATED_PROFILE.ecu_specification = "valid" 2827 2828 else: 2829 _GENERATED_PROFILE.ecu = BadStates.INVALID_RESPONSE.name 2830 logger.write_warning_to_html_report("Could not get ECU information") 2831 _GENERATED_PROFILE.ecu_specification = "invalid"
| Description | Get information from ECUID response |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#395 |
| Instructions | 1. Request ECUID data 2. Log returned values |
| Estimated Duration | 30 seconds |
2833 def test_comparison(self) -> None: 2834 """ 2835 | Description | Compare ECU Information | 2836 | :------------------- | :--------------------------------------------------------------- | 2837 | GitHub Issue | turnaroundfactor/BMS-HW-Test#395 | 2838 | Instructions | 1. Confirm identity numbers are unique for each battery | 2839 | Pass / Fail Criteria | Pass if ECU values match / serial number is unique | 2840 | Estimated Duration | 1 second | 2841 """ 2842 2843 failed_test = False 2844 if not hasattr(_GENERATED_PROFILE, "ecu") or not hasattr(_COMPARISON_PROFILE, "ecu"): 2845 logger.write_result_to_html_report("Nothing to compare. The profile test may have failed or been skipped") 2846 pytest.skip("Nothing to compare. The profile test may have failed or been skipped") 2847 2848 if isinstance(_GENERATED_PROFILE.ecu, str) or isinstance(_COMPARISON_PROFILE.ecu, str): 2849 if not isinstance(_GENERATED_PROFILE.ecu, type(_COMPARISON_PROFILE.ecu)): 2850 message = "Unable to compare profiles, ECU types did not match" 2851 logger.write_failure_to_html_report(message) 2852 pytest.fail(message) 2853 2854 comparison = cmp(_GENERATED_PROFILE.ecu, "==", _COMPARISON_PROFILE.ecu) 2855 2856 if _GENERATED_PROFILE.ecu != _COMPARISON_PROFILE.ecu: 2857 message = f"ECU did not match: {comparison}" 2858 logger.write_failure_to_html_report(message) 2859 pytest.fail(message) 2860 2861 message = f"ECU matched: {comparison}" 2862 logger.write_result_to_html_report(message) 2863 else: 2864 2865 failed_categories = [] 2866 2867 for key, value in _GENERATED_PROFILE.ecu.items(): 2868 key_text = key.replace("_", " ").title() 2869 2870 comparison = cmp(value, "==", _COMPARISON_PROFILE.ecu[key]) 2871 2872 if key == "serial_number": 2873 if value == _COMPARISON_PROFILE.ecu[key]: 2874 message = f"{key_text} matched: {comparison}" 2875 logger.write_warning_to_html_report(message) 2876 else: 2877 message = f"{key_text}: {comparison}" 2878 logger.write_result_to_html_report(message) 2879 2880 elif value != _COMPARISON_PROFILE.ecu[key]: 2881 message = f"{key_text} did not match: {comparison}" 2882 logger.write_warning_to_html_report(message) 2883 failed_categories.append(key_text) 2884 else: 2885 message = f"{key_text}: {comparison}" 2886 logger.write_result_to_html_report(message) 2887 2888 fail_length = len(failed_categories) 2889 2890 if fail_length > 0: 2891 categories = "data values" if fail_length > 1 else "data value" 2892 message = f"{fail_length} {categories} failed profile comparisons: {', '.join(failed_categories)}" 2893 logger.write_failure_to_html_report(message) 2894 failed_test = True 2895 2896 if not hasattr(_GENERATED_PROFILE, "ecu_specification") or not hasattr( 2897 _COMPARISON_PROFILE, "ecu_specification" 2898 ): 2899 logger.write_warning_to_html_report("Missing specification information") 2900 else: 2901 comparison = cmp( 2902 _GENERATED_PROFILE.ecu_specification, 2903 "==", 2904 _COMPARISON_PROFILE.ecu_specification, 2905 ) 2906 if _GENERATED_PROFILE.ecu_specification != _COMPARISON_PROFILE.ecu_specification: 2907 logger.write_failure_to_html_report(f"Specification comparison failed: {comparison}") 2908 failed_test = True 2909 else: 2910 logger.write_result_to_html_report(f"Specification comparison passed: {comparison}") 2911 2912 if failed_test: 2913 message = "Comparisons did not meet expectations" 2914 logger.write_failure_to_html_report(message) 2915 pytest.fail(message) 2916 else: 2917 logger.write_result_to_html_report("ECUID profile comparisons met requirements")
| Description | Compare ECU Information |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#395 |
| Instructions | 1. Confirm identity numbers are unique for each battery |
| Pass / Fail Criteria | Pass if ECU values match / serial number is unique |
| Estimated Duration | 1 second |
2920class TestAnalyzeTimingCommands: 2921 """Test Supported Commands & Conduct Timing Analysis""" 2922 2923 @pytest.mark.profile 2924 def test_analyze_timing_commands(self) -> None: 2925 """ 2926 | Description | Analyze timing of commands | 2927 | :------------------- | :--------------------------------------------------------------- | 2928 | GitHub Issue | turnaroundfactor/BMS-HW-Test#383 | 2929 | Instructions | 1. Make requests for supported commands </br>\ 2930 2. Conduct timing analysis </br>\ 2931 3. Log average times | 2932 | Estimated Duration | 35 seconds | 2933 """ 2934 2935 with CANBus(BATTERY_CHANNEL) as bus: 2936 j1939_transmit_commands = [0xEE00, 0xFEE6, 0xFCB6, 0xFECA, 0xFECB, 0xFE50, 0xFDC5, 0xFEDA, 0xD800, 0xE800] 2937 mil_prf_commands = [0xFF00, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08] 2938 if hasattr(_GENERATED_PROFILE, "proprietary_commands"): 2939 proprietary_commands = list(_GENERATED_PROFILE.proprietary_commands.keys()) 2940 else: 2941 proprietary_commands = [] 2942 2943 # Remove potential duplicate commands 2944 commands_list = list(dict.fromkeys(j1939_transmit_commands + mil_prf_commands + proprietary_commands)) 2945 2946 times_list: dict[int | Any, list[Any]] = {} 2947 average_times = {} 2948 no_valid_response: dict[int, bool] = {} 2949 for _ in range(0, 3): 2950 for command in commands_list: 2951 command_name = PGN[command].name 2952 if no_valid_response.get(command): 2953 continue 2954 2955 request_frame = CANFrame( 2956 destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[command] 2957 ) 2958 start_time = time.perf_counter() 2959 if response := bus.process_call(request_frame): 2960 if response.pgn.id == PGN["TP.CM", [32]].id: 2961 expected_bytes = int(response.data[1]) 2962 expected_packets = 0 2963 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.BRENTRONICS: 2964 expected_packets = int(response.data[2] + 1) 2965 else: 2966 expected_packets = int(response.data[2]) 2967 2968 rts_pgn_id = int(response.data[-1]) 2969 if command in (PGN["ECUID", [32]].id, PGN["SOFT", [32]].id): 2970 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 2971 else: 2972 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 2973 cts_request.data[1] = expected_packets 2974 cts_request.data[-1] = rts_pgn_id 2975 2976 bus.send_message(cts_request.message()) 2977 for i in range(expected_packets): 2978 data_frame = bus.read_frame() 2979 if data_frame is None: 2980 message = ( 2981 f"Received no data packet when in TP.CM protocol for command" 2982 f" {command}, {command_name}" 2983 ) 2984 logger.write_warning_to_html_report(message) 2985 if data_frame.pgn.id != PGN["TP.DT", [32]].id: 2986 message = ( 2987 f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id} " 2988 f"for PGN request {command} ({command_name}) instead" 2989 ) 2990 logger.write_warning_to_html_report(message) 2991 2992 # Send acknowledgement frame 2993 end_time = time.perf_counter() 2994 end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 2995 end_acknowledge_frame.data[1] = expected_bytes 2996 end_acknowledge_frame.data[2] = expected_packets 2997 end_acknowledge_frame.data[-1] = rts_pgn_id 2998 bus.send_message(end_acknowledge_frame.message()) 2999 else: 3000 end_time = time.perf_counter() 3001 3002 if response.pgn.id in (command, PGN["TP.CM", [32]].id): 3003 if not times_list.get(command): 3004 times_list[command] = [end_time - start_time] 3005 else: 3006 times_list[command].append(end_time - start_time) 3007 else: 3008 if response.pgn.id == PGN["ACKM", [32]].id: 3009 message = ( 3010 f"PGN {command} ({command_name}): Received PGN {response.pgn.id} " 3011 f"({spn_types.acknowledgement(response.data[0])}) " 3012 ) 3013 average_times[command] = ( 3014 f"PGN {response.pgn.id} ({spn_types.acknowledgement(response.data[0])})" 3015 ) 3016 else: 3017 message = ( 3018 f"PGN {command} ({command_name}): Received PGN {response.pgn.id} " 3019 f"({response.pgn.name})." 3020 ) 3021 average_times[command] = f"PGN {response.pgn.id} ({response.pgn.name})" 3022 logger.write_warning_to_html_report(message) 3023 no_valid_response[command] = True 3024 continue 3025 else: 3026 logger.write_warning_to_html_report( 3027 f"Did not receive any response for PGN {command} ({command_name})" 3028 ) 3029 no_valid_response[command] = True 3030 average_times[command] = "No response" 3031 continue 3032 3033 for pgn, item in times_list.items(): 3034 times = item 3035 average = sum(times) / len(times) 3036 average_times[pgn] = average 3037 message = f"PGN {pgn} ({PGN[pgn].name}) average time: {average:.6f} seconds" 3038 logger.write_result_to_html_report(message) 3039 3040 _GENERATED_PROFILE.average_times = average_times 3041 3042 def test_comparison(self) -> None: 3043 """ 3044 | Description | Compare average time values | 3045 | :------------------- | :--------------------------------------------------------------- | 3046 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 3047 | Instructions | 1. Check if profiles complete commands in approx. same time | 3048 | Pass / Fail Criteria | Pass if profiles time completion are closely matched | 3049 | Estimated Duration | 1 second | 3050 """ 3051 3052 if not hasattr(_GENERATED_PROFILE, "average_times") or not hasattr(_COMPARISON_PROFILE, "average_times"): 3053 message = "Nothing to compare. The profile test may have failed or been skipped" 3054 logger.write_failure_to_html_report(message) 3055 pytest.fail(message) 3056 3057 generated_averages = _GENERATED_PROFILE.average_times 3058 comparison_averages = _COMPARISON_PROFILE.average_times 3059 3060 averages_not_close = [] 3061 missing_commands = [] 3062 percent_closeness = 0.50 3063 for pgn in generated_averages: 3064 if not comparison_averages.get(str(pgn)): 3065 logger.write_warning_to_html_report(f"Missing PGN {pgn} ({PGN[pgn].name}) from comparison profile") 3066 missing_commands.append(pgn) 3067 continue 3068 3069 if isinstance(generated_averages[pgn], str) or isinstance(comparison_averages[str(pgn)], str): 3070 if generated_averages[pgn] != comparison_averages[str(pgn)]: 3071 logger.write_warning_to_html_report( 3072 f"PGN {pgn} ({PGN[pgn].name}): {generated_averages[pgn]} ≠ {comparison_averages[str(pgn)]}" 3073 ) 3074 else: 3075 logger.write_result_to_html_report( 3076 f"PGN {pgn} ({PGN[pgn].name}): {generated_averages[pgn]} = {comparison_averages[str(pgn)]}" 3077 ) 3078 continue 3079 3080 diff = abs(comparison_averages[str(pgn)] - generated_averages[pgn]) 3081 average = (generated_averages[pgn] + comparison_averages[str(pgn)]) / 2 3082 percentage = round((diff / average), 4) 3083 comparison = cmp( 3084 percentage * 100, 3085 "<=", 3086 percent_closeness * 100, 3087 "%", 3088 ) 3089 3090 if not math.isclose( 3091 generated_averages[pgn], 3092 comparison_averages[str(pgn)], 3093 rel_tol=percent_closeness, 3094 ): 3095 logger.write_warning_to_html_report(f"PGN {pgn} ({PGN[pgn].name}) average percentage: {comparison}") 3096 averages_not_close.append(pgn) 3097 else: 3098 logger.write_result_to_html_report(f"PGN {pgn} ({PGN[pgn].name}) average percentage: {comparison}") 3099 3100 length_not_close = len(averages_not_close) 3101 if length_not_close > 0: 3102 logger.write_failure_to_html_report( 3103 f"{length_not_close} comparison{'s' if length_not_close > 1 else ''} " 3104 f"did not meet expectations. See warnings above for more details." 3105 ) 3106 pytest.fail("Some comparison times were not within range") 3107 3108 if len(missing_commands) > 0: 3109 logger.write_failure_to_html_report(f"{len(missing_commands)} PGNs were missing from comparison profile") 3110 3111 logger.write_result_to_html_report( 3112 f"All PGN Average times for profiles were within expected range of {percent_closeness * 100}%" 3113 )
Test Supported Commands & Conduct Timing Analysis
2923 @pytest.mark.profile 2924 def test_analyze_timing_commands(self) -> None: 2925 """ 2926 | Description | Analyze timing of commands | 2927 | :------------------- | :--------------------------------------------------------------- | 2928 | GitHub Issue | turnaroundfactor/BMS-HW-Test#383 | 2929 | Instructions | 1. Make requests for supported commands </br>\ 2930 2. Conduct timing analysis </br>\ 2931 3. Log average times | 2932 | Estimated Duration | 35 seconds | 2933 """ 2934 2935 with CANBus(BATTERY_CHANNEL) as bus: 2936 j1939_transmit_commands = [0xEE00, 0xFEE6, 0xFCB6, 0xFECA, 0xFECB, 0xFE50, 0xFDC5, 0xFEDA, 0xD800, 0xE800] 2937 mil_prf_commands = [0xFF00, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08] 2938 if hasattr(_GENERATED_PROFILE, "proprietary_commands"): 2939 proprietary_commands = list(_GENERATED_PROFILE.proprietary_commands.keys()) 2940 else: 2941 proprietary_commands = [] 2942 2943 # Remove potential duplicate commands 2944 commands_list = list(dict.fromkeys(j1939_transmit_commands + mil_prf_commands + proprietary_commands)) 2945 2946 times_list: dict[int | Any, list[Any]] = {} 2947 average_times = {} 2948 no_valid_response: dict[int, bool] = {} 2949 for _ in range(0, 3): 2950 for command in commands_list: 2951 command_name = PGN[command].name 2952 if no_valid_response.get(command): 2953 continue 2954 2955 request_frame = CANFrame( 2956 destination_address=_GENERATED_PROFILE.address, pgn=PGN["RQST"], data=[command] 2957 ) 2958 start_time = time.perf_counter() 2959 if response := bus.process_call(request_frame): 2960 if response.pgn.id == PGN["TP.CM", [32]].id: 2961 expected_bytes = int(response.data[1]) 2962 expected_packets = 0 2963 if _GENERATED_PROFILE.manufacturer_code == ManufacturerID.BRENTRONICS: 2964 expected_packets = int(response.data[2] + 1) 2965 else: 2966 expected_packets = int(response.data[2]) 2967 2968 rts_pgn_id = int(response.data[-1]) 2969 if command in (PGN["ECUID", [32]].id, PGN["SOFT", [32]].id): 2970 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[17, 0, 1, 0xFF, 0]) 2971 else: 2972 cts_request = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 2973 cts_request.data[1] = expected_packets 2974 cts_request.data[-1] = rts_pgn_id 2975 2976 bus.send_message(cts_request.message()) 2977 for i in range(expected_packets): 2978 data_frame = bus.read_frame() 2979 if data_frame is None: 2980 message = ( 2981 f"Received no data packet when in TP.CM protocol for command" 2982 f" {command}, {command_name}" 2983 ) 2984 logger.write_warning_to_html_report(message) 2985 if data_frame.pgn.id != PGN["TP.DT", [32]].id: 2986 message = ( 2987 f"Expected data frame {i + 1}, got PGN {data_frame.pgn.id} " 2988 f"for PGN request {command} ({command_name}) instead" 2989 ) 2990 logger.write_warning_to_html_report(message) 2991 2992 # Send acknowledgement frame 2993 end_time = time.perf_counter() 2994 end_acknowledge_frame = CANFrame(pgn=PGN["TP.CM"], data=[19, 0, 0, 0xFF, 0]) 2995 end_acknowledge_frame.data[1] = expected_bytes 2996 end_acknowledge_frame.data[2] = expected_packets 2997 end_acknowledge_frame.data[-1] = rts_pgn_id 2998 bus.send_message(end_acknowledge_frame.message()) 2999 else: 3000 end_time = time.perf_counter() 3001 3002 if response.pgn.id in (command, PGN["TP.CM", [32]].id): 3003 if not times_list.get(command): 3004 times_list[command] = [end_time - start_time] 3005 else: 3006 times_list[command].append(end_time - start_time) 3007 else: 3008 if response.pgn.id == PGN["ACKM", [32]].id: 3009 message = ( 3010 f"PGN {command} ({command_name}): Received PGN {response.pgn.id} " 3011 f"({spn_types.acknowledgement(response.data[0])}) " 3012 ) 3013 average_times[command] = ( 3014 f"PGN {response.pgn.id} ({spn_types.acknowledgement(response.data[0])})" 3015 ) 3016 else: 3017 message = ( 3018 f"PGN {command} ({command_name}): Received PGN {response.pgn.id} " 3019 f"({response.pgn.name})." 3020 ) 3021 average_times[command] = f"PGN {response.pgn.id} ({response.pgn.name})" 3022 logger.write_warning_to_html_report(message) 3023 no_valid_response[command] = True 3024 continue 3025 else: 3026 logger.write_warning_to_html_report( 3027 f"Did not receive any response for PGN {command} ({command_name})" 3028 ) 3029 no_valid_response[command] = True 3030 average_times[command] = "No response" 3031 continue 3032 3033 for pgn, item in times_list.items(): 3034 times = item 3035 average = sum(times) / len(times) 3036 average_times[pgn] = average 3037 message = f"PGN {pgn} ({PGN[pgn].name}) average time: {average:.6f} seconds" 3038 logger.write_result_to_html_report(message) 3039 3040 _GENERATED_PROFILE.average_times = average_times
| Description | Analyze timing of commands |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#383 |
| Instructions | 1. Make requests for supported commands 2. Conduct timing analysis 3. Log average times |
| Estimated Duration | 35 seconds |
3042 def test_comparison(self) -> None: 3043 """ 3044 | Description | Compare average time values | 3045 | :------------------- | :--------------------------------------------------------------- | 3046 | GitHub Issue | turnaroundfactor/BMS-HW-Test#393 | 3047 | Instructions | 1. Check if profiles complete commands in approx. same time | 3048 | Pass / Fail Criteria | Pass if profiles time completion are closely matched | 3049 | Estimated Duration | 1 second | 3050 """ 3051 3052 if not hasattr(_GENERATED_PROFILE, "average_times") or not hasattr(_COMPARISON_PROFILE, "average_times"): 3053 message = "Nothing to compare. The profile test may have failed or been skipped" 3054 logger.write_failure_to_html_report(message) 3055 pytest.fail(message) 3056 3057 generated_averages = _GENERATED_PROFILE.average_times 3058 comparison_averages = _COMPARISON_PROFILE.average_times 3059 3060 averages_not_close = [] 3061 missing_commands = [] 3062 percent_closeness = 0.50 3063 for pgn in generated_averages: 3064 if not comparison_averages.get(str(pgn)): 3065 logger.write_warning_to_html_report(f"Missing PGN {pgn} ({PGN[pgn].name}) from comparison profile") 3066 missing_commands.append(pgn) 3067 continue 3068 3069 if isinstance(generated_averages[pgn], str) or isinstance(comparison_averages[str(pgn)], str): 3070 if generated_averages[pgn] != comparison_averages[str(pgn)]: 3071 logger.write_warning_to_html_report( 3072 f"PGN {pgn} ({PGN[pgn].name}): {generated_averages[pgn]} ≠ {comparison_averages[str(pgn)]}" 3073 ) 3074 else: 3075 logger.write_result_to_html_report( 3076 f"PGN {pgn} ({PGN[pgn].name}): {generated_averages[pgn]} = {comparison_averages[str(pgn)]}" 3077 ) 3078 continue 3079 3080 diff = abs(comparison_averages[str(pgn)] - generated_averages[pgn]) 3081 average = (generated_averages[pgn] + comparison_averages[str(pgn)]) / 2 3082 percentage = round((diff / average), 4) 3083 comparison = cmp( 3084 percentage * 100, 3085 "<=", 3086 percent_closeness * 100, 3087 "%", 3088 ) 3089 3090 if not math.isclose( 3091 generated_averages[pgn], 3092 comparison_averages[str(pgn)], 3093 rel_tol=percent_closeness, 3094 ): 3095 logger.write_warning_to_html_report(f"PGN {pgn} ({PGN[pgn].name}) average percentage: {comparison}") 3096 averages_not_close.append(pgn) 3097 else: 3098 logger.write_result_to_html_report(f"PGN {pgn} ({PGN[pgn].name}) average percentage: {comparison}") 3099 3100 length_not_close = len(averages_not_close) 3101 if length_not_close > 0: 3102 logger.write_failure_to_html_report( 3103 f"{length_not_close} comparison{'s' if length_not_close > 1 else ''} " 3104 f"did not meet expectations. See warnings above for more details." 3105 ) 3106 pytest.fail("Some comparison times were not within range") 3107 3108 if len(missing_commands) > 0: 3109 logger.write_failure_to_html_report(f"{len(missing_commands)} PGNs were missing from comparison profile") 3110 3111 logger.write_result_to_html_report( 3112 f"All PGN Average times for profiles were within expected range of {percent_closeness * 100}%" 3113 )
| Description | Compare average time values |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#393 |
| Instructions | 1. Check if profiles complete commands in approx. same time |
| Pass / Fail Criteria | Pass if profiles time completion are closely matched |
| Estimated Duration | 1 second |
3116def memory_test(mode: Modes) -> list[int]: 3117 """Memory tester helper""" 3118 found_addresses = [] 3119 with CANBus(BATTERY_CHANNEL) as bus: 3120 read_request_frame = CANFrame(pgn=PGN["DM14"], data=[1, 1, 1, 0, 0, 0, 0, 0]) 3121 read_request_frame.data[2] = mode 3122 addresses = [0, 1107296256, 2147483648, 4294966271] # 0x0, 0x42000000, 0x80000000, 0xfffffbff 3123 for low_address in addresses: 3124 found = 0 3125 high_address = low_address + (1024 if FULL_MEMORY_TESTS else 16) 3126 for i in range(low_address, high_address): 3127 read_request_frame.data[5] = low_address 3128 if response_frame := bus.process_call(read_request_frame, timeout=1): # NOTE: SAFT times out 3129 if response_frame is not None: 3130 logger.write_info_to_report( 3131 f"Address {i} responded with PGN {response_frame.pgn.id} " 3132 f"({response_frame.pgn.short_name}) - status is: " 3133 f"{spn_types.dm15_status(response_frame.data[3])}" 3134 ) 3135 if response_frame.pgn.id == PGN["DM15"].id: 3136 if response_frame.data[5] != 258: # Invalid Length 3137 if response_frame.data[3] == 0: 3138 found_addresses.append(i) 3139 found += 1 3140 else: 3141 Errors.unexpected_packet("DM15", response_frame) 3142 break 3143 else: 3144 Errors.no_packet("DM15") 3145 break 3146 else: 3147 # timeout 3148 pass 3149 verb = "" 3150 match mode: 3151 case Modes.READ: 3152 verb = "readable" 3153 case Modes.WRITE: 3154 verb = "writable" 3155 case Modes.ERASE: 3156 verb = "erasable" 3157 case Modes.BOOT: 3158 verb = "boot load" 3159 message = ( 3160 f"Found {found} {verb} successful address(es) in memory ranges" 3161 f" {hex(low_address)}-{hex(high_address)}" 3162 ) 3163 logger.write_result_to_html_report(message) 3164 3165 if len(found_addresses) > 0: 3166 logger.write_result_to_html_report( 3167 f"Found {len(found_addresses)} {verb} memory address(es) out of " 3168 f"{4096 if FULL_MEMORY_TESTS else 64} possible addresses" 3169 ) 3170 else: 3171 message = f"Found 0 {verb} memory addresses out of {4096 if FULL_MEMORY_TESTS else 64} possible addresses" 3172 logger.write_warning_to_html_report(message) 3173 return found_addresses
Memory tester helper
3176class TestMemoryRead: 3177 """This will test the read capability of the memory""" 3178 3179 @pytest.mark.profile 3180 def test_read(self) -> None: 3181 """ 3182 | Description | Try to read from different memory locations | 3183 | :------------------- | :--------------------------------------------------------------- | 3184 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3185 turnaroundfactor/BMS-HW-Test#380 | 3186 | Instructions | 1. Request memory read </br>\ 3187 2. Log any successful addresses | 3188 | Estimated Duration | 85 seconds | 3189 """ 3190 3191 _GENERATED_PROFILE.read_addresses = memory_test(Modes.READ) 3192 3193 def test_comparison(self) -> None: 3194 """ 3195 | Description | Compare read results | 3196 | :------------------- | :--------------------------------------------------------------- | 3197 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3198 turnaroundfactor/BMS-HW-Test#380 | 3199 | Instructions | Confirm no new memory addresses are readable | 3200 | Pass / Fail Criteria | Pass if no new readable memory addresses | 3201 | Estimated Duration | 1 second | 3202 """ 3203 3204 if hasattr(_GENERATED_PROFILE, "read_addresses") and hasattr(_COMPARISON_PROFILE, "read_addresses"): 3205 new_addresses = set(_GENERATED_PROFILE.read_addresses) - set(_COMPARISON_PROFILE.read_addresses) 3206 if new_addresses: 3207 message = ( 3208 f"{len(new_addresses)} Different readable memory address(es) found: " 3209 f"{', '.join(map(str, new_addresses))}" 3210 ) 3211 logger.write_failure_to_html_report(message) 3212 pytest.fail(message) 3213 3214 logger.write_result_to_html_report("Profiles had matching readable memory address responses") 3215 3216 else: 3217 message = "Nothing to compare. The profile test may have failed or been skipped" 3218 logger.write_result_to_html_report(message) 3219 pytest.skip(message)
This will test the read capability of the memory
3179 @pytest.mark.profile 3180 def test_read(self) -> None: 3181 """ 3182 | Description | Try to read from different memory locations | 3183 | :------------------- | :--------------------------------------------------------------- | 3184 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3185 turnaroundfactor/BMS-HW-Test#380 | 3186 | Instructions | 1. Request memory read </br>\ 3187 2. Log any successful addresses | 3188 | Estimated Duration | 85 seconds | 3189 """ 3190 3191 _GENERATED_PROFILE.read_addresses = memory_test(Modes.READ)
| Description | Try to read from different memory locations |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#379 turnaroundfactor/BMS-HW-Test#380 |
| Instructions | 1. Request memory read 2. Log any successful addresses |
| Estimated Duration | 85 seconds |
3193 def test_comparison(self) -> None: 3194 """ 3195 | Description | Compare read results | 3196 | :------------------- | :--------------------------------------------------------------- | 3197 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3198 turnaroundfactor/BMS-HW-Test#380 | 3199 | Instructions | Confirm no new memory addresses are readable | 3200 | Pass / Fail Criteria | Pass if no new readable memory addresses | 3201 | Estimated Duration | 1 second | 3202 """ 3203 3204 if hasattr(_GENERATED_PROFILE, "read_addresses") and hasattr(_COMPARISON_PROFILE, "read_addresses"): 3205 new_addresses = set(_GENERATED_PROFILE.read_addresses) - set(_COMPARISON_PROFILE.read_addresses) 3206 if new_addresses: 3207 message = ( 3208 f"{len(new_addresses)} Different readable memory address(es) found: " 3209 f"{', '.join(map(str, new_addresses))}" 3210 ) 3211 logger.write_failure_to_html_report(message) 3212 pytest.fail(message) 3213 3214 logger.write_result_to_html_report("Profiles had matching readable memory address responses") 3215 3216 else: 3217 message = "Nothing to compare. The profile test may have failed or been skipped" 3218 logger.write_result_to_html_report(message) 3219 pytest.skip(message)
| Description | Compare read results |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#379 turnaroundfactor/BMS-HW-Test#380 |
| Instructions | Confirm no new memory addresses are readable |
| Pass / Fail Criteria | Pass if no new readable memory addresses |
| Estimated Duration | 1 second |
3222class TestMemoryWrite: 3223 """This will test the write capability of the memory""" 3224 3225 @pytest.mark.profile 3226 def test_write(self) -> None: 3227 """ 3228 | Description | Try to write to different memory locations | 3229 | :------------------- | :--------------------------------------------------------------- | 3230 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3231 turnaroundfactor/BMS-HW-Test#380 | 3232 | Instructions | 1. Request memory write </br>\ 3233 2. Log any successful addresses | 3234 | Estimated Duration | 85 seconds | 3235 """ 3236 3237 _GENERATED_PROFILE.write_addresses = memory_test(Modes.WRITE) 3238 3239 def test_comparison(self) -> None: 3240 """ 3241 | Description | Compare write results | 3242 | :------------------- | :--------------------------------------------------------------- | 3243 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3244 turnaroundfactor/BMS-HW-Test#380 | 3245 | Instructions | Confirm no new memory addresses are writable | 3246 | Pass / Fail Criteria | Pass if no new writable memory addresses | 3247 | Estimated Duration | 1 second | 3248 """ 3249 3250 if hasattr(_GENERATED_PROFILE, "write_addresses") and hasattr(_COMPARISON_PROFILE, "write_addresses"): 3251 new_addresses = set(_GENERATED_PROFILE.write_addresses) - set(_COMPARISON_PROFILE.write_addresses) 3252 if new_addresses: 3253 message = ( 3254 f"{len(new_addresses)} Different writable memory address(es) found: " 3255 f"{', '.join(map(str, new_addresses))}" 3256 ) 3257 logger.write_failure_to_html_report(message) 3258 pytest.fail(message) 3259 3260 logger.write_result_to_html_report("Profiles had matching writable memory address responses") 3261 3262 else: 3263 message = "Nothing to compare. The profile test may have failed or been skipped" 3264 logger.write_result_to_html_report(message) 3265 pytest.skip(message)
This will test the write capability of the memory
3225 @pytest.mark.profile 3226 def test_write(self) -> None: 3227 """ 3228 | Description | Try to write to different memory locations | 3229 | :------------------- | :--------------------------------------------------------------- | 3230 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3231 turnaroundfactor/BMS-HW-Test#380 | 3232 | Instructions | 1. Request memory write </br>\ 3233 2. Log any successful addresses | 3234 | Estimated Duration | 85 seconds | 3235 """ 3236 3237 _GENERATED_PROFILE.write_addresses = memory_test(Modes.WRITE)
| Description | Try to write to different memory locations |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#379 turnaroundfactor/BMS-HW-Test#380 |
| Instructions | 1. Request memory write 2. Log any successful addresses |
| Estimated Duration | 85 seconds |
3239 def test_comparison(self) -> None: 3240 """ 3241 | Description | Compare write results | 3242 | :------------------- | :--------------------------------------------------------------- | 3243 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3244 turnaroundfactor/BMS-HW-Test#380 | 3245 | Instructions | Confirm no new memory addresses are writable | 3246 | Pass / Fail Criteria | Pass if no new writable memory addresses | 3247 | Estimated Duration | 1 second | 3248 """ 3249 3250 if hasattr(_GENERATED_PROFILE, "write_addresses") and hasattr(_COMPARISON_PROFILE, "write_addresses"): 3251 new_addresses = set(_GENERATED_PROFILE.write_addresses) - set(_COMPARISON_PROFILE.write_addresses) 3252 if new_addresses: 3253 message = ( 3254 f"{len(new_addresses)} Different writable memory address(es) found: " 3255 f"{', '.join(map(str, new_addresses))}" 3256 ) 3257 logger.write_failure_to_html_report(message) 3258 pytest.fail(message) 3259 3260 logger.write_result_to_html_report("Profiles had matching writable memory address responses") 3261 3262 else: 3263 message = "Nothing to compare. The profile test may have failed or been skipped" 3264 logger.write_result_to_html_report(message) 3265 pytest.skip(message)
| Description | Compare write results |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#379 turnaroundfactor/BMS-HW-Test#380 |
| Instructions | Confirm no new memory addresses are writable |
| Pass / Fail Criteria | Pass if no new writable memory addresses |
| Estimated Duration | 1 second |
3268class TestMemoryErase: 3269 """This will test the erase capability of the memory""" 3270 3271 @pytest.mark.profile 3272 def test_erase(self) -> None: 3273 """ 3274 | Description | Try to erase different memory locations | 3275 | :------------------- | :--------------------------------------------------------------- | 3276 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3277 turnaroundfactor/BMS-HW-Test#380 | 3278 | Instructions | 1. Request memory erase </br>\ 3279 2. Log any successful addresses | 3280 | Estimated Duration | 85 seconds | 3281 """ 3282 3283 _GENERATED_PROFILE.erase_addresses = memory_test(Modes.ERASE) 3284 3285 def test_comparison(self) -> None: 3286 """ 3287 | Description | Compare erase results | 3288 | :------------------- | :--------------------------------------------------------------- | 3289 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3290 turnaroundfactor/BMS-HW-Test#380 | 3291 | Instructions | Confirm no new memory addresses are erasable | 3292 | Pass / Fail Criteria | Pass if no new erasable memory addresses | 3293 | Estimated Duration | 1 second | 3294 """ 3295 3296 if hasattr(_GENERATED_PROFILE, "erase_addresses") and hasattr(_COMPARISON_PROFILE, "erase_addresses"): 3297 new_addresses = set(_GENERATED_PROFILE.erase_addresses) - set(_COMPARISON_PROFILE.erase_addresses) 3298 if new_addresses: 3299 message = ( 3300 f"{len(new_addresses)} Different erasable memory address(es) found: " 3301 f"{', '.join(map(str, new_addresses))}" 3302 ) 3303 logger.write_failure_to_html_report(message) 3304 pytest.fail(message) 3305 3306 logger.write_result_to_html_report("Profiles had matching erasable memory address responses") 3307 3308 else: 3309 message = "Nothing to compare. The profile test may have failed or been skipped" 3310 logger.write_result_to_html_report(message) 3311 pytest.skip(message)
This will test the erase capability of the memory
3271 @pytest.mark.profile 3272 def test_erase(self) -> None: 3273 """ 3274 | Description | Try to erase different memory locations | 3275 | :------------------- | :--------------------------------------------------------------- | 3276 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3277 turnaroundfactor/BMS-HW-Test#380 | 3278 | Instructions | 1. Request memory erase </br>\ 3279 2. Log any successful addresses | 3280 | Estimated Duration | 85 seconds | 3281 """ 3282 3283 _GENERATED_PROFILE.erase_addresses = memory_test(Modes.ERASE)
| Description | Try to erase different memory locations |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#379 turnaroundfactor/BMS-HW-Test#380 |
| Instructions | 1. Request memory erase 2. Log any successful addresses |
| Estimated Duration | 85 seconds |
3285 def test_comparison(self) -> None: 3286 """ 3287 | Description | Compare erase results | 3288 | :------------------- | :--------------------------------------------------------------- | 3289 | GitHub Issue | turnaroundfactor/BMS-HW-Test#379 </br>\ 3290 turnaroundfactor/BMS-HW-Test#380 | 3291 | Instructions | Confirm no new memory addresses are erasable | 3292 | Pass / Fail Criteria | Pass if no new erasable memory addresses | 3293 | Estimated Duration | 1 second | 3294 """ 3295 3296 if hasattr(_GENERATED_PROFILE, "erase_addresses") and hasattr(_COMPARISON_PROFILE, "erase_addresses"): 3297 new_addresses = set(_GENERATED_PROFILE.erase_addresses) - set(_COMPARISON_PROFILE.erase_addresses) 3298 if new_addresses: 3299 message = ( 3300 f"{len(new_addresses)} Different erasable memory address(es) found: " 3301 f"{', '.join(map(str, new_addresses))}" 3302 ) 3303 logger.write_failure_to_html_report(message) 3304 pytest.fail(message) 3305 3306 logger.write_result_to_html_report("Profiles had matching erasable memory address responses") 3307 3308 else: 3309 message = "Nothing to compare. The profile test may have failed or been skipped" 3310 logger.write_result_to_html_report(message) 3311 pytest.skip(message)
| Description | Compare erase results |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#379 turnaroundfactor/BMS-HW-Test#380 |
| Instructions | Confirm no new memory addresses are erasable |
| Pass / Fail Criteria | Pass if no new erasable memory addresses |
| Estimated Duration | 1 second |
3314class TestMemoryBootLoad: 3315 """This will test the boot load capability of the memory""" 3316 3317 @pytest.mark.profile 3318 def test_boot(self) -> None: 3319 """ 3320 | Description | Try to boot load from different memory locations | 3321 | :------------------- | :--------------------------------------------------------------- | 3322 | GitHub Issue | turnaroundfactor/BMS-HW-Test#381 </br>\ 3323 turnaroundfactor/BMS-HW-Test#382 | 3324 | Instructions | 1. Request memory boot load </br>\ 3325 2. Log any successful addresses | 3326 | Estimated Duration | 85 seconds | 3327 """ 3328 3329 _GENERATED_PROFILE.boot_addresses = memory_test(Modes.BOOT) 3330 3331 def test_comparison(self) -> None: 3332 """ 3333 | Description | Compare boot load results | 3334 | :------------------- | :--------------------------------------------------------------- | 3335 | GitHub Issue | turnaroundfactor/BMS-HW-Test#381 </br>\ 3336 turnaroundfactor/BMS-HW-Test#382 | 3337 | Instructions | Confirm no new memory addresses are boot load capable | 3338 | Pass / Fail Criteria | Pass if no new boot load memory addresses | 3339 | Estimated Duration | 1 second | 3340 """ 3341 3342 if hasattr(_GENERATED_PROFILE, "boot_addresses") and hasattr(_COMPARISON_PROFILE, "boot_addresses"): 3343 new_addresses = set(_GENERATED_PROFILE.boot_addresses) - set(_COMPARISON_PROFILE.boot_addresses) 3344 if new_addresses: 3345 message = ( 3346 f"{len(new_addresses)} Different boot memory address(es) found: " 3347 f"{', '.join(map(str, new_addresses))}" 3348 ) 3349 logger.write_failure_to_html_report(message) 3350 pytest.fail(message) 3351 3352 logger.write_result_to_html_report("Profiles had matching boot memory address responses") 3353 3354 else: 3355 message = "Nothing to compare. The profile test may have failed or been skipped" 3356 logger.write_result_to_html_report(message) 3357 pytest.skip(message)
This will test the boot load capability of the memory
3317 @pytest.mark.profile 3318 def test_boot(self) -> None: 3319 """ 3320 | Description | Try to boot load from different memory locations | 3321 | :------------------- | :--------------------------------------------------------------- | 3322 | GitHub Issue | turnaroundfactor/BMS-HW-Test#381 </br>\ 3323 turnaroundfactor/BMS-HW-Test#382 | 3324 | Instructions | 1. Request memory boot load </br>\ 3325 2. Log any successful addresses | 3326 | Estimated Duration | 85 seconds | 3327 """ 3328 3329 _GENERATED_PROFILE.boot_addresses = memory_test(Modes.BOOT)
| Description | Try to boot load from different memory locations |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#381 turnaroundfactor/BMS-HW-Test#382 |
| Instructions | 1. Request memory boot load 2. Log any successful addresses |
| Estimated Duration | 85 seconds |
3331 def test_comparison(self) -> None: 3332 """ 3333 | Description | Compare boot load results | 3334 | :------------------- | :--------------------------------------------------------------- | 3335 | GitHub Issue | turnaroundfactor/BMS-HW-Test#381 </br>\ 3336 turnaroundfactor/BMS-HW-Test#382 | 3337 | Instructions | Confirm no new memory addresses are boot load capable | 3338 | Pass / Fail Criteria | Pass if no new boot load memory addresses | 3339 | Estimated Duration | 1 second | 3340 """ 3341 3342 if hasattr(_GENERATED_PROFILE, "boot_addresses") and hasattr(_COMPARISON_PROFILE, "boot_addresses"): 3343 new_addresses = set(_GENERATED_PROFILE.boot_addresses) - set(_COMPARISON_PROFILE.boot_addresses) 3344 if new_addresses: 3345 message = ( 3346 f"{len(new_addresses)} Different boot memory address(es) found: " 3347 f"{', '.join(map(str, new_addresses))}" 3348 ) 3349 logger.write_failure_to_html_report(message) 3350 pytest.fail(message) 3351 3352 logger.write_result_to_html_report("Profiles had matching boot memory address responses") 3353 3354 else: 3355 message = "Nothing to compare. The profile test may have failed or been skipped" 3356 logger.write_result_to_html_report(message) 3357 pytest.skip(message)
| Description | Compare boot load results |
|---|---|
| GitHub Issue | turnaroundfactor/BMS-HW-Test#381 turnaroundfactor/BMS-HW-Test#382 |
| Instructions | Confirm no new memory addresses are boot load capable |
| Pass / Fail Criteria | Pass if no new boot load memory addresses |
| Estimated Duration | 1 second |