hitl_tester.modules.bms.smbus

Functions for accessing SMBus.

(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.

  1"""
  2Functions for accessing SMBus.
  3
  4# (c) 2020-2024 TurnAround Factor, Inc.
  5#
  6# CUI DISTRIBUTION CONTROL
  7# Controlled by: DLA J68 R&D SBIP
  8# CUI Category: Small Business Research and Technology
  9# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
 10# POC: GOV SBIP Program Manager Denise Price, 571-767-0111
 11# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
 12# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
 13# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
 14# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
 15# Fort Belvoir, VA 22060-6221
 16#
 17# SBIR DATA RIGHTS
 18# Contract No.:SP4701-23-C-0083
 19# Contractor Name: TurnAround Factor, Inc.
 20# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
 21# Expiration of SBIR Data Rights Period: September 24, 2029
 22# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
 23# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
 24# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
 25# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
 26# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
 27# the markings.
 28"""
 29
 30from __future__ import annotations
 31
 32import errno
 33from string import Template
 34from typing import ClassVar, cast
 35
 36import pytest
 37from smbus2 import SMBus as PySMBus
 38from typing_extensions import Self
 39
 40from hitl_tester.modules.bms_types import BMSFlags
 41from hitl_tester.modules.bms.smbus_types import SMBusReg, SMBusRegType, BatteryMode, SMBusFunctionType, SMBusError
 42
 43SMBUS_ADDRESS = 0xB
 44
 45
 46class SMBus:
 47    """SMBus object"""
 48
 49    instance: ClassVar[Self | None] = None
 50
 51    def __new__(cls):
 52        """Make SMBus a singleton."""
 53        if cls.instance is None:
 54            cls.instance = super().__new__(cls)
 55        return cls.instance
 56
 57    def read_register(self, register: SMBusReg) -> tuple[SMBusRegType | None, bytes]:
 58        """Read from an SMBus register."""
 59        assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
 60        channel = cast(str, pytest.flags.config["smbus_i2c_channel"])
 61
 62        size = register.value.size
 63        try:
 64            with PySMBus(channel) as bus:
 65                if block_read := size is None:  # Simulate an SMBus block read as the Pi driver doesn't support it
 66                    size = min(32, bus.read_i2c_block_data(SMBUS_ADDRESS, register.value.address, 1)[0] + 1)
 67                data = bytes(bus.read_i2c_block_data(SMBUS_ADDRESS, register.value.address, size))[block_read:]
 68        except OSError as e:
 69            raise SMBusError(
 70                f"{errno.errorcode[e.errno]} ({e.strerror}) when reading "
 71                f"{register.value.size} byte{'' if register.value.size==1 else 's'} from register "
 72                f"0x{register.value.address:02X} ({register.name}), channel {channel}, "
 73                f"SMBus address 0x{SMBUS_ADDRESS:02X}"
 74            ) from e
 75
 76        try:
 77            return register.value.type.value(data), data
 78        except (ValueError, IndexError, KeyError):
 79            return None, data
 80
 81    def write_register(self, register: SMBusReg, word: int):
 82        """Write to an SMBus register."""
 83        if not register.value.writable:
 84            raise RuntimeError("Attempted to write to read-only register.")
 85
 86        assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
 87        channel = cast(str, pytest.flags.config["smbus_i2c_channel"])
 88
 89        try:
 90            with PySMBus(channel) as bus:
 91                bus.write_word_data(SMBUS_ADDRESS, register.value.address, word)
 92        except OSError as e:
 93            raise SMBusError(
 94                f"{errno.errorcode[e.errno]} ({e.strerror}) when writing "
 95                f"0x{word:04X} to register 0x{register.value.address:02X} ({register.name}), channel {channel}, "
 96                f"SMBus address 0x{SMBUS_ADDRESS:02X})"
 97            ) from e
 98
 99    def parse_smbus_data(self) -> dict[str, SMBusRegType | None]:
100        """Return a dict with all smbus data."""
101
102        def to_title(text: str) -> str:
103            """Format titles."""
104            return text.title().replace("_", " ")
105
106        try:
107            parsed_battery_mode = cast(BatteryMode, self.read_register(SMBusReg.BATTERY_MODE)[0])
108        except SMBusError:
109            parsed_battery_mode = BatteryMode(bytes())
110        capacity_unit = "cW" if parsed_battery_mode.capacity_mode else "mA"
111
112        smbus_data: dict[str, SMBusRegType | None] = {}
113        for register in SMBusReg:
114            unit = Template(register.value.unit).safe_substitute(capacity=capacity_unit)
115            title = f"{to_title(register.name)}{f' ({unit})' if unit else ''}"
116            try:
117                register_value, register_raw = self.read_register(register)
118            except SMBusError:
119                register_value, register_raw = None, bytes()
120
121            if register in (SMBusReg.SPECIFICATION_INFO, SMBusReg.BATTERY_STATUS, SMBusReg.BATTERY_MODE):
122                register_value = register_value or register.value.type.value(bytes([0x31]))  # Create dummy object
123                for attribute in register_value.__dict__:
124                    smbus_data[f"{title} {to_title(attribute)}"] = str(getattr(register_value, attribute))
125                if register is SMBusReg.BATTERY_MODE:
126                    smbus_data[f"{title} (Raw)"] = f"0x{register_raw.hex().upper()}"
127            else:
128                smbus_data[title] = register_value
129                if register.value.type not in (
130                    SMBusFunctionType.INT,
131                    SMBusFunctionType.UNSIGNED_INT,
132                    SMBusFunctionType.HEX,
133                ):
134                    smbus_data[f"{title} (Raw)"] = f"0x{register_raw.hex().upper()}"
135
136        return smbus_data
SMBUS_ADDRESS = 11
class SMBus:
 47class SMBus:
 48    """SMBus object"""
 49
 50    instance: ClassVar[Self | None] = None
 51
 52    def __new__(cls):
 53        """Make SMBus a singleton."""
 54        if cls.instance is None:
 55            cls.instance = super().__new__(cls)
 56        return cls.instance
 57
 58    def read_register(self, register: SMBusReg) -> tuple[SMBusRegType | None, bytes]:
 59        """Read from an SMBus register."""
 60        assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
 61        channel = cast(str, pytest.flags.config["smbus_i2c_channel"])
 62
 63        size = register.value.size
 64        try:
 65            with PySMBus(channel) as bus:
 66                if block_read := size is None:  # Simulate an SMBus block read as the Pi driver doesn't support it
 67                    size = min(32, bus.read_i2c_block_data(SMBUS_ADDRESS, register.value.address, 1)[0] + 1)
 68                data = bytes(bus.read_i2c_block_data(SMBUS_ADDRESS, register.value.address, size))[block_read:]
 69        except OSError as e:
 70            raise SMBusError(
 71                f"{errno.errorcode[e.errno]} ({e.strerror}) when reading "
 72                f"{register.value.size} byte{'' if register.value.size==1 else 's'} from register "
 73                f"0x{register.value.address:02X} ({register.name}), channel {channel}, "
 74                f"SMBus address 0x{SMBUS_ADDRESS:02X}"
 75            ) from e
 76
 77        try:
 78            return register.value.type.value(data), data
 79        except (ValueError, IndexError, KeyError):
 80            return None, data
 81
 82    def write_register(self, register: SMBusReg, word: int):
 83        """Write to an SMBus register."""
 84        if not register.value.writable:
 85            raise RuntimeError("Attempted to write to read-only register.")
 86
 87        assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
 88        channel = cast(str, pytest.flags.config["smbus_i2c_channel"])
 89
 90        try:
 91            with PySMBus(channel) as bus:
 92                bus.write_word_data(SMBUS_ADDRESS, register.value.address, word)
 93        except OSError as e:
 94            raise SMBusError(
 95                f"{errno.errorcode[e.errno]} ({e.strerror}) when writing "
 96                f"0x{word:04X} to register 0x{register.value.address:02X} ({register.name}), channel {channel}, "
 97                f"SMBus address 0x{SMBUS_ADDRESS:02X})"
 98            ) from e
 99
100    def parse_smbus_data(self) -> dict[str, SMBusRegType | None]:
101        """Return a dict with all smbus data."""
102
103        def to_title(text: str) -> str:
104            """Format titles."""
105            return text.title().replace("_", " ")
106
107        try:
108            parsed_battery_mode = cast(BatteryMode, self.read_register(SMBusReg.BATTERY_MODE)[0])
109        except SMBusError:
110            parsed_battery_mode = BatteryMode(bytes())
111        capacity_unit = "cW" if parsed_battery_mode.capacity_mode else "mA"
112
113        smbus_data: dict[str, SMBusRegType | None] = {}
114        for register in SMBusReg:
115            unit = Template(register.value.unit).safe_substitute(capacity=capacity_unit)
116            title = f"{to_title(register.name)}{f' ({unit})' if unit else ''}"
117            try:
118                register_value, register_raw = self.read_register(register)
119            except SMBusError:
120                register_value, register_raw = None, bytes()
121
122            if register in (SMBusReg.SPECIFICATION_INFO, SMBusReg.BATTERY_STATUS, SMBusReg.BATTERY_MODE):
123                register_value = register_value or register.value.type.value(bytes([0x31]))  # Create dummy object
124                for attribute in register_value.__dict__:
125                    smbus_data[f"{title} {to_title(attribute)}"] = str(getattr(register_value, attribute))
126                if register is SMBusReg.BATTERY_MODE:
127                    smbus_data[f"{title} (Raw)"] = f"0x{register_raw.hex().upper()}"
128            else:
129                smbus_data[title] = register_value
130                if register.value.type not in (
131                    SMBusFunctionType.INT,
132                    SMBusFunctionType.UNSIGNED_INT,
133                    SMBusFunctionType.HEX,
134                ):
135                    smbus_data[f"{title} (Raw)"] = f"0x{register_raw.hex().upper()}"
136
137        return smbus_data

SMBus object

SMBus()
52    def __new__(cls):
53        """Make SMBus a singleton."""
54        if cls.instance is None:
55            cls.instance = super().__new__(cls)
56        return cls.instance

Make SMBus a singleton.

instance: ClassVar[Optional[Self]] = <SMBus object>
def read_register( self, register: hitl_tester.modules.bms.smbus_types.SMBusReg) -> tuple[typing.Union[int, bool, hitl_tester.modules.bms.smbus_types.BatteryStatus, hitl_tester.modules.bms.smbus_types.BatteryMode, hitl_tester.modules.bms.smbus_types.SpecInfo, str, datetime.datetime, NoneType], bytes]:
58    def read_register(self, register: SMBusReg) -> tuple[SMBusRegType | None, bytes]:
59        """Read from an SMBus register."""
60        assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
61        channel = cast(str, pytest.flags.config["smbus_i2c_channel"])
62
63        size = register.value.size
64        try:
65            with PySMBus(channel) as bus:
66                if block_read := size is None:  # Simulate an SMBus block read as the Pi driver doesn't support it
67                    size = min(32, bus.read_i2c_block_data(SMBUS_ADDRESS, register.value.address, 1)[0] + 1)
68                data = bytes(bus.read_i2c_block_data(SMBUS_ADDRESS, register.value.address, size))[block_read:]
69        except OSError as e:
70            raise SMBusError(
71                f"{errno.errorcode[e.errno]} ({e.strerror}) when reading "
72                f"{register.value.size} byte{'' if register.value.size==1 else 's'} from register "
73                f"0x{register.value.address:02X} ({register.name}), channel {channel}, "
74                f"SMBus address 0x{SMBUS_ADDRESS:02X}"
75            ) from e
76
77        try:
78            return register.value.type.value(data), data
79        except (ValueError, IndexError, KeyError):
80            return None, data

Read from an SMBus register.

def write_register( self, register: hitl_tester.modules.bms.smbus_types.SMBusReg, word: int):
82    def write_register(self, register: SMBusReg, word: int):
83        """Write to an SMBus register."""
84        if not register.value.writable:
85            raise RuntimeError("Attempted to write to read-only register.")
86
87        assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags)
88        channel = cast(str, pytest.flags.config["smbus_i2c_channel"])
89
90        try:
91            with PySMBus(channel) as bus:
92                bus.write_word_data(SMBUS_ADDRESS, register.value.address, word)
93        except OSError as e:
94            raise SMBusError(
95                f"{errno.errorcode[e.errno]} ({e.strerror}) when writing "
96                f"0x{word:04X} to register 0x{register.value.address:02X} ({register.name}), channel {channel}, "
97                f"SMBus address 0x{SMBUS_ADDRESS:02X})"
98            ) from e

Write to an SMBus register.

def parse_smbus_data( self) -> dict[str, typing.Union[int, bool, hitl_tester.modules.bms.smbus_types.BatteryStatus, hitl_tester.modules.bms.smbus_types.BatteryMode, hitl_tester.modules.bms.smbus_types.SpecInfo, str, datetime.datetime, NoneType]]:
100    def parse_smbus_data(self) -> dict[str, SMBusRegType | None]:
101        """Return a dict with all smbus data."""
102
103        def to_title(text: str) -> str:
104            """Format titles."""
105            return text.title().replace("_", " ")
106
107        try:
108            parsed_battery_mode = cast(BatteryMode, self.read_register(SMBusReg.BATTERY_MODE)[0])
109        except SMBusError:
110            parsed_battery_mode = BatteryMode(bytes())
111        capacity_unit = "cW" if parsed_battery_mode.capacity_mode else "mA"
112
113        smbus_data: dict[str, SMBusRegType | None] = {}
114        for register in SMBusReg:
115            unit = Template(register.value.unit).safe_substitute(capacity=capacity_unit)
116            title = f"{to_title(register.name)}{f' ({unit})' if unit else ''}"
117            try:
118                register_value, register_raw = self.read_register(register)
119            except SMBusError:
120                register_value, register_raw = None, bytes()
121
122            if register in (SMBusReg.SPECIFICATION_INFO, SMBusReg.BATTERY_STATUS, SMBusReg.BATTERY_MODE):
123                register_value = register_value or register.value.type.value(bytes([0x31]))  # Create dummy object
124                for attribute in register_value.__dict__:
125                    smbus_data[f"{title} {to_title(attribute)}"] = str(getattr(register_value, attribute))
126                if register is SMBusReg.BATTERY_MODE:
127                    smbus_data[f"{title} (Raw)"] = f"0x{register_raw.hex().upper()}"
128            else:
129                smbus_data[title] = register_value
130                if register.value.type not in (
131                    SMBusFunctionType.INT,
132                    SMBusFunctionType.UNSIGNED_INT,
133                    SMBusFunctionType.HEX,
134                ):
135                    smbus_data[f"{title} (Raw)"] = f"0x{register_raw.hex().upper()}"
136
137        return smbus_data

Return a dict with all smbus data.