hitl_tester.modules.bms.chroma_sim
Provides controls for the Chroma 16CH Cell Simulator.
(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""" 2Provides controls for the Chroma 16CH Cell Simulator. 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 30import time 31from contextlib import suppress 32from enum import IntEnum, IntFlag 33 34from hitl_tester.modules.bms.cell import Cell 35from hitl_tester.modules.bms_types import UnderVoltageError, OverVoltageError, CellCompMode, SafeResource 36from hitl_tester.modules.logger import logger 37 38CURRENT_LIMIT = 5.0 39 40 41class CellTestingStatus(IntEnum): 42 """Cell testing status.""" 43 44 RUNNING = 0 45 STOP_BY_IPC = 1 46 STOP_BY_PROTECTION = 2 47 STOP_BY_ERROR = 3 48 STOP_BY_EMERGENCY_OFF = 4 49 50 51class CellOutputStatus(IntEnum): 52 """Cell output status.""" 53 54 IDLE = 0 55 TESTING = 1 56 STOP = 2 57 58 59class CellProtection(IntFlag): 60 """Cell protection status.""" 61 62 NO_ERROR = 0 63 OCP = 2 64 WIRELOSS = 8 65 FAN_FAIL = 16 66 POWER_FAIL = 32 67 FAN_SPEED_WARNING = 256 68 OLDP = 512 69 70 71class ChromaCell(Cell): 72 """Chroma 16CH Cell Simulator command wrapper.""" 73 74 def __init__(self, cell_id: int, resource: SafeResource, cell_chemistry: str, cell_slot: int): 75 self.slot = cell_slot 76 super().__init__(cell_id, resource, cell_chemistry) 77 78 @property 79 def measured_volts(self) -> float: 80 """Measures actual cell voltage.""" 81 with self._lock: 82 result = float(self._resource.query(f"SIM:MEAS:CELL:VOLT? 1,{self.slot},{self.slot}")) 83 return result if result != 9.91e37 else 0.0 # Convert NaN to 0 84 85 @property 86 def volts(self) -> float: 87 """Gets the last target voltage.""" 88 with self._lock: 89 return self._volts 90 91 @volts.setter 92 def volts(self, new_voltage: float): 93 """Sets the output voltage.""" 94 with self._lock: 95 if not self.disengage_safety_protocols and new_voltage <= self.data.uv_protection: 96 raise UnderVoltageError( 97 f"Undervoltage protection triggered at {time.strftime('%x %X')} on cell {self.id}, slot {self.slot}" 98 f" Voltage {new_voltage} is lower than {self.data.uv_protection}." 99 ) 100 if not self.disengage_safety_protocols and new_voltage >= self.data.ov_protection: 101 raise OverVoltageError( 102 f"Overvoltage protection triggered at {time.strftime('%x %X')} on cell {self.id}, slot {self.slot}." 103 f" Voltage {new_voltage} is higher than {self.data.ov_protection}." 104 ) 105 106 self._resource.write(f"SIM:PROG:CELL 1,1,{self.slot},{self.slot},{new_voltage},{CURRENT_LIMIT}") 107 self._volts = new_voltage 108 self._resource.write("SIM:OUTP:IMM") 109 self.enable() # FIXME(JA): explicitly enable cells 110 111 @property 112 def ohms(self) -> float: 113 """Measures internal resistance of the cell. Not supported.""" 114 with self._lock: 115 return self._resistance 116 117 @ohms.setter 118 def ohms(self, new_ohms: float): 119 """Sets the internal resistance of the cell. Not supported.""" 120 with self._lock: 121 self._resistance = new_ohms 122 123 @property 124 def amps(self) -> float: 125 """Measures cell current.""" 126 with self._lock: 127 result = float(self._resource.query(f"SIM:MEAS:CELL:CURR? 1,{self.slot},{self.slot}")) 128 return result if result != 9.91e37 else 0.0 # Convert NaN to 0 129 130 @amps.setter 131 def amps(self, new_amps: float): 132 """Sets the current limit of the cell (current≥0:charge, current<0:discharge).""" 133 global CURRENT_LIMIT 134 with self._lock: 135 CURRENT_LIMIT = new_amps 136 self.volts = float(self._resource.query(f"SIM:PROG:CELL? 1,1,{self.slot},{self.slot}").split(",")[2]) 137 138 @property 139 def compensation_mode(self) -> CellCompMode: 140 """Sets compensation mode""" 141 return CellCompMode.LLOCAL 142 143 @compensation_mode.setter 144 def compensation_mode(self, mode: CellCompMode): 145 """Sets compensation mode""" 146 147 def enable(self): 148 """Enables the cell.""" 149 self._resource.write(f"SIM:OUTP:SPE ON,1,{self.slot},{self.slot}") 150 # with suppress(ValueError): # Useful for debugging 151 # protection = CellProtection(int(self._resource.query(f"SIM:MEAS:CELL:PROT? 1,{self.slot},{self.slot}"))) 152 # logger.write_debug_to_report(f"Protection is: {protection} ({protection!s})") 153 self._thread_pause_flag.set() 154 155 def disable(self): 156 """Disables the cell.""" 157 self._thread_pause_flag.clear() 158 self._resource.write(f"SIM:OUTP:SPE OFF,1,{self.slot},{self.slot}") 159 160 def is_enabled(self) -> bool: 161 """Check output status.""" 162 return bool(self._resource.query(f"SIM:MEAS:CELL:OPER? 1,{self.slot},{self.slot}")) 163 164 def reset(self): 165 """Resets the instrument""" 166 self._resource.write("*CLS") # Clear the Error Queue and other status registers 167 168 logger.write_debug_to_report(f"Identification String: {self._resource.query('*IDN?')}") 169 logger.write_debug_to_report(f"Status: {self._resource.query('SYST:FRAME:STAT? 0')}") 170 logger.write_debug_to_report(f"Master Frame info: {self._resource.query('SYST:FRAME? 1')}") 171 logger.write_debug_to_report(f"Channels Status: {self._resource.query('SYST:FRAME:CHAN:STAT? 0')}") 172 logger.write_debug_to_report(f"Unit Channel: {self._resource.query('SYST:FRAME:CHAN:NUMB? 0')}") 173 logger.write_debug_to_report(f"Errors?: {self._resource.query('SYST:ERROR?')}") 174 175 self._resource.write("SYST:FRAME:PROT:CLE") # Clears the protection status of all devices 176 self._resource.write("SIM:CONF:CLE") # Clears UUT settings and resets them the default settings 177 self._resource.write("SIM:CONF:BMS:NUMB 1") # 1 BMS 178 self._resource.write("SIM:CONF:SAMP:TIME 10") # 10ms sample time 179 self._resource.write("SIM:CONF:CELL:NUMB 1,16") # 16 cells 180 self._resource.write("SIM:CONF:CELL:PARA 1,1,8,2,4") # current range (2 = 5A, 4 = 10A) 181 182 time.sleep(6) # Needs delay after reset 183 184 def output_status(self) -> CellOutputStatus | None: 185 """Read cell output status.""" 186 with suppress(ValueError): 187 return CellOutputStatus(int(self._resource.query(f"SIM:MEAS:CELL:OPER? 1,{self.slot},{self.slot}"))) 188 return None 189 190 def testing_status(self) -> CellTestingStatus | None: 191 """Read cell testing status.""" 192 with suppress(ValueError): 193 return CellTestingStatus(int(self._resource.query(f"SIM:MEAS:CELL:STAT? 1,{self.slot},{self.slot}"))) 194 return None 195 196 def write(self, command: str): 197 """Issue an SCPI write command.""" 198 self._resource.write(command) 199 200 def query(self, command: str) -> str: 201 """Issue an SCPI query command.""" 202 return str(self._resource.query(command))
CURRENT_LIMIT =
5.0
class
CellTestingStatus(enum.IntEnum):
42class CellTestingStatus(IntEnum): 43 """Cell testing status.""" 44 45 RUNNING = 0 46 STOP_BY_IPC = 1 47 STOP_BY_PROTECTION = 2 48 STOP_BY_ERROR = 3 49 STOP_BY_EMERGENCY_OFF = 4
Cell testing status.
RUNNING =
<CellTestingStatus.RUNNING: 0>
STOP_BY_IPC =
<CellTestingStatus.STOP_BY_IPC: 1>
STOP_BY_PROTECTION =
<CellTestingStatus.STOP_BY_PROTECTION: 2>
STOP_BY_ERROR =
<CellTestingStatus.STOP_BY_ERROR: 3>
STOP_BY_EMERGENCY_OFF =
<CellTestingStatus.STOP_BY_EMERGENCY_OFF: 4>
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
class
CellOutputStatus(enum.IntEnum):
52class CellOutputStatus(IntEnum): 53 """Cell output status.""" 54 55 IDLE = 0 56 TESTING = 1 57 STOP = 2
Cell output status.
IDLE =
<CellOutputStatus.IDLE: 0>
TESTING =
<CellOutputStatus.TESTING: 1>
STOP =
<CellOutputStatus.STOP: 2>
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
class
CellProtection(enum.IntFlag):
60class CellProtection(IntFlag): 61 """Cell protection status.""" 62 63 NO_ERROR = 0 64 OCP = 2 65 WIRELOSS = 8 66 FAN_FAIL = 16 67 POWER_FAIL = 32 68 FAN_SPEED_WARNING = 256 69 OLDP = 512
Cell protection status.
NO_ERROR =
<CellProtection.NO_ERROR: 0>
OCP =
<CellProtection.OCP: 2>
WIRELOSS =
<CellProtection.WIRELOSS: 8>
FAN_FAIL =
<CellProtection.FAN_FAIL: 16>
POWER_FAIL =
<CellProtection.POWER_FAIL: 32>
FAN_SPEED_WARNING =
<CellProtection.FAN_SPEED_WARNING: 256>
OLDP =
<CellProtection.OLDP: 512>
Inherited Members
- builtins.int
- conjugate
- bit_length
- bit_count
- to_bytes
- from_bytes
- as_integer_ratio
- is_integer
- real
- imag
- numerator
- denominator
- enum.Enum
- name
- value
72class ChromaCell(Cell): 73 """Chroma 16CH Cell Simulator command wrapper.""" 74 75 def __init__(self, cell_id: int, resource: SafeResource, cell_chemistry: str, cell_slot: int): 76 self.slot = cell_slot 77 super().__init__(cell_id, resource, cell_chemistry) 78 79 @property 80 def measured_volts(self) -> float: 81 """Measures actual cell voltage.""" 82 with self._lock: 83 result = float(self._resource.query(f"SIM:MEAS:CELL:VOLT? 1,{self.slot},{self.slot}")) 84 return result if result != 9.91e37 else 0.0 # Convert NaN to 0 85 86 @property 87 def volts(self) -> float: 88 """Gets the last target voltage.""" 89 with self._lock: 90 return self._volts 91 92 @volts.setter 93 def volts(self, new_voltage: float): 94 """Sets the output voltage.""" 95 with self._lock: 96 if not self.disengage_safety_protocols and new_voltage <= self.data.uv_protection: 97 raise UnderVoltageError( 98 f"Undervoltage protection triggered at {time.strftime('%x %X')} on cell {self.id}, slot {self.slot}" 99 f" Voltage {new_voltage} is lower than {self.data.uv_protection}." 100 ) 101 if not self.disengage_safety_protocols and new_voltage >= self.data.ov_protection: 102 raise OverVoltageError( 103 f"Overvoltage protection triggered at {time.strftime('%x %X')} on cell {self.id}, slot {self.slot}." 104 f" Voltage {new_voltage} is higher than {self.data.ov_protection}." 105 ) 106 107 self._resource.write(f"SIM:PROG:CELL 1,1,{self.slot},{self.slot},{new_voltage},{CURRENT_LIMIT}") 108 self._volts = new_voltage 109 self._resource.write("SIM:OUTP:IMM") 110 self.enable() # FIXME(JA): explicitly enable cells 111 112 @property 113 def ohms(self) -> float: 114 """Measures internal resistance of the cell. Not supported.""" 115 with self._lock: 116 return self._resistance 117 118 @ohms.setter 119 def ohms(self, new_ohms: float): 120 """Sets the internal resistance of the cell. Not supported.""" 121 with self._lock: 122 self._resistance = new_ohms 123 124 @property 125 def amps(self) -> float: 126 """Measures cell current.""" 127 with self._lock: 128 result = float(self._resource.query(f"SIM:MEAS:CELL:CURR? 1,{self.slot},{self.slot}")) 129 return result if result != 9.91e37 else 0.0 # Convert NaN to 0 130 131 @amps.setter 132 def amps(self, new_amps: float): 133 """Sets the current limit of the cell (current≥0:charge, current<0:discharge).""" 134 global CURRENT_LIMIT 135 with self._lock: 136 CURRENT_LIMIT = new_amps 137 self.volts = float(self._resource.query(f"SIM:PROG:CELL? 1,1,{self.slot},{self.slot}").split(",")[2]) 138 139 @property 140 def compensation_mode(self) -> CellCompMode: 141 """Sets compensation mode""" 142 return CellCompMode.LLOCAL 143 144 @compensation_mode.setter 145 def compensation_mode(self, mode: CellCompMode): 146 """Sets compensation mode""" 147 148 def enable(self): 149 """Enables the cell.""" 150 self._resource.write(f"SIM:OUTP:SPE ON,1,{self.slot},{self.slot}") 151 # with suppress(ValueError): # Useful for debugging 152 # protection = CellProtection(int(self._resource.query(f"SIM:MEAS:CELL:PROT? 1,{self.slot},{self.slot}"))) 153 # logger.write_debug_to_report(f"Protection is: {protection} ({protection!s})") 154 self._thread_pause_flag.set() 155 156 def disable(self): 157 """Disables the cell.""" 158 self._thread_pause_flag.clear() 159 self._resource.write(f"SIM:OUTP:SPE OFF,1,{self.slot},{self.slot}") 160 161 def is_enabled(self) -> bool: 162 """Check output status.""" 163 return bool(self._resource.query(f"SIM:MEAS:CELL:OPER? 1,{self.slot},{self.slot}")) 164 165 def reset(self): 166 """Resets the instrument""" 167 self._resource.write("*CLS") # Clear the Error Queue and other status registers 168 169 logger.write_debug_to_report(f"Identification String: {self._resource.query('*IDN?')}") 170 logger.write_debug_to_report(f"Status: {self._resource.query('SYST:FRAME:STAT? 0')}") 171 logger.write_debug_to_report(f"Master Frame info: {self._resource.query('SYST:FRAME? 1')}") 172 logger.write_debug_to_report(f"Channels Status: {self._resource.query('SYST:FRAME:CHAN:STAT? 0')}") 173 logger.write_debug_to_report(f"Unit Channel: {self._resource.query('SYST:FRAME:CHAN:NUMB? 0')}") 174 logger.write_debug_to_report(f"Errors?: {self._resource.query('SYST:ERROR?')}") 175 176 self._resource.write("SYST:FRAME:PROT:CLE") # Clears the protection status of all devices 177 self._resource.write("SIM:CONF:CLE") # Clears UUT settings and resets them the default settings 178 self._resource.write("SIM:CONF:BMS:NUMB 1") # 1 BMS 179 self._resource.write("SIM:CONF:SAMP:TIME 10") # 10ms sample time 180 self._resource.write("SIM:CONF:CELL:NUMB 1,16") # 16 cells 181 self._resource.write("SIM:CONF:CELL:PARA 1,1,8,2,4") # current range (2 = 5A, 4 = 10A) 182 183 time.sleep(6) # Needs delay after reset 184 185 def output_status(self) -> CellOutputStatus | None: 186 """Read cell output status.""" 187 with suppress(ValueError): 188 return CellOutputStatus(int(self._resource.query(f"SIM:MEAS:CELL:OPER? 1,{self.slot},{self.slot}"))) 189 return None 190 191 def testing_status(self) -> CellTestingStatus | None: 192 """Read cell testing status.""" 193 with suppress(ValueError): 194 return CellTestingStatus(int(self._resource.query(f"SIM:MEAS:CELL:STAT? 1,{self.slot},{self.slot}"))) 195 return None 196 197 def write(self, command: str): 198 """Issue an SCPI write command.""" 199 self._resource.write(command) 200 201 def query(self, command: str) -> str: 202 """Issue an SCPI query command.""" 203 return str(self._resource.query(command))
Chroma 16CH Cell Simulator command wrapper.
ChromaCell( cell_id: int, resource: hitl_tester.modules.bms_types.SafeResource, cell_chemistry: str, cell_slot: int)
75 def __init__(self, cell_id: int, resource: SafeResource, cell_chemistry: str, cell_slot: int): 76 self.slot = cell_slot 77 super().__init__(cell_id, resource, cell_chemistry)
Initialize the 66321 wrapper with a specific PyVISA resource.
measured_volts: float
79 @property 80 def measured_volts(self) -> float: 81 """Measures actual cell voltage.""" 82 with self._lock: 83 result = float(self._resource.query(f"SIM:MEAS:CELL:VOLT? 1,{self.slot},{self.slot}")) 84 return result if result != 9.91e37 else 0.0 # Convert NaN to 0
Measures actual cell voltage.
volts: float
86 @property 87 def volts(self) -> float: 88 """Gets the last target voltage.""" 89 with self._lock: 90 return self._volts
Gets the last target voltage.
ohms: float
112 @property 113 def ohms(self) -> float: 114 """Measures internal resistance of the cell. Not supported.""" 115 with self._lock: 116 return self._resistance
Measures internal resistance of the cell. Not supported.
amps: float
124 @property 125 def amps(self) -> float: 126 """Measures cell current.""" 127 with self._lock: 128 result = float(self._resource.query(f"SIM:MEAS:CELL:CURR? 1,{self.slot},{self.slot}")) 129 return result if result != 9.91e37 else 0.0 # Convert NaN to 0
Measures cell current.
compensation_mode: hitl_tester.modules.bms_types.CellCompMode
139 @property 140 def compensation_mode(self) -> CellCompMode: 141 """Sets compensation mode""" 142 return CellCompMode.LLOCAL
Sets compensation mode
def
enable(self):
148 def enable(self): 149 """Enables the cell.""" 150 self._resource.write(f"SIM:OUTP:SPE ON,1,{self.slot},{self.slot}") 151 # with suppress(ValueError): # Useful for debugging 152 # protection = CellProtection(int(self._resource.query(f"SIM:MEAS:CELL:PROT? 1,{self.slot},{self.slot}"))) 153 # logger.write_debug_to_report(f"Protection is: {protection} ({protection!s})") 154 self._thread_pause_flag.set()
Enables the cell.
def
disable(self):
156 def disable(self): 157 """Disables the cell.""" 158 self._thread_pause_flag.clear() 159 self._resource.write(f"SIM:OUTP:SPE OFF,1,{self.slot},{self.slot}")
Disables the cell.
def
is_enabled(self) -> bool:
161 def is_enabled(self) -> bool: 162 """Check output status.""" 163 return bool(self._resource.query(f"SIM:MEAS:CELL:OPER? 1,{self.slot},{self.slot}"))
Check output status.
def
reset(self):
165 def reset(self): 166 """Resets the instrument""" 167 self._resource.write("*CLS") # Clear the Error Queue and other status registers 168 169 logger.write_debug_to_report(f"Identification String: {self._resource.query('*IDN?')}") 170 logger.write_debug_to_report(f"Status: {self._resource.query('SYST:FRAME:STAT? 0')}") 171 logger.write_debug_to_report(f"Master Frame info: {self._resource.query('SYST:FRAME? 1')}") 172 logger.write_debug_to_report(f"Channels Status: {self._resource.query('SYST:FRAME:CHAN:STAT? 0')}") 173 logger.write_debug_to_report(f"Unit Channel: {self._resource.query('SYST:FRAME:CHAN:NUMB? 0')}") 174 logger.write_debug_to_report(f"Errors?: {self._resource.query('SYST:ERROR?')}") 175 176 self._resource.write("SYST:FRAME:PROT:CLE") # Clears the protection status of all devices 177 self._resource.write("SIM:CONF:CLE") # Clears UUT settings and resets them the default settings 178 self._resource.write("SIM:CONF:BMS:NUMB 1") # 1 BMS 179 self._resource.write("SIM:CONF:SAMP:TIME 10") # 10ms sample time 180 self._resource.write("SIM:CONF:CELL:NUMB 1,16") # 16 cells 181 self._resource.write("SIM:CONF:CELL:PARA 1,1,8,2,4") # current range (2 = 5A, 4 = 10A) 182 183 time.sleep(6) # Needs delay after reset
Resets the instrument
185 def output_status(self) -> CellOutputStatus | None: 186 """Read cell output status.""" 187 with suppress(ValueError): 188 return CellOutputStatus(int(self._resource.query(f"SIM:MEAS:CELL:OPER? 1,{self.slot},{self.slot}"))) 189 return None
Read cell output status.
191 def testing_status(self) -> CellTestingStatus | None: 192 """Read cell testing status.""" 193 with suppress(ValueError): 194 return CellTestingStatus(int(self._resource.query(f"SIM:MEAS:CELL:STAT? 1,{self.slot},{self.slot}"))) 195 return None
Read cell testing status.
def
write(self, command: str):
197 def write(self, command: str): 198 """Issue an SCPI write command.""" 199 self._resource.write(command)
Issue an SCPI write command.