hitl_tester.modules.bms.thermal_chamber

This API communicates with a Watlow F4T controller through TCP, allowing the HITL to control the thermal chamber that's attached to the controller. For register information, see "F4T Modbus Registers (Map 1)" pg 227 in "F4T Setup Operation 16802414 Rev A".

(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"""
  2This API communicates with a Watlow F4T controller through TCP, allowing the HITL to control the thermal chamber
  3that's attached to the controller.
  4For register information, see "F4T Modbus Registers (Map 1)" pg 227 in "F4T Setup Operation 16802414 Rev A".
  5
  6# (c) 2020-2024 TurnAround Factor, Inc.
  7#
  8# CUI DISTRIBUTION CONTROL
  9# Controlled by: DLA J68 R&D SBIP
 10# CUI Category: Small Business Research and Technology
 11# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
 12# POC: GOV SBIP Program Manager Denise Price, 571-767-0111
 13# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
 14# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
 15# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
 16# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
 17# Fort Belvoir, VA 22060-6221
 18#
 19# SBIR DATA RIGHTS
 20# Contract No.:SP4701-23-C-0083
 21# Contractor Name: TurnAround Factor, Inc.
 22# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
 23# Expiration of SBIR Data Rights Period: September 24, 2029
 24# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
 25# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
 26# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
 27# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
 28# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
 29# the markings.
 30"""
 31
 32from __future__ import annotations
 33
 34import atexit
 35import time
 36from datetime import datetime, timedelta
 37from typing import cast
 38
 39import pytest
 40
 41from hitl_tester.modules.bms_types import TimeoutExceededError, BMSFlags, SafeResource
 42from hitl_tester.modules.logger import logger
 43
 44
 45class ThermalChamber:
 46    """Watlow F4T controller command wrapper."""
 47
 48    def __init__(self):
 49        """Initialize the Watlow F4T controller."""
 50        if (
 51            hasattr(pytest, "flags")
 52            and isinstance(pytest.flags, BMSFlags)
 53            and "thermal_chamber_address" in pytest.flags.config
 54        ):
 55            logger.write_info_to_report("Connecting to thermal chamber...")
 56            ip = pytest.flags.config["thermal_chamber_address"]
 57            self.resource = SafeResource((f"TCPIP::{ip}::5025::SOCKET", "\n", "\n"))
 58            self.internal_units = "C"
 59            self.display_units = "C"
 60            self._timedelta = timedelta()
 61
 62        @atexit.register
 63        def __atexit__():
 64            """Configure a safe shut down for when the class instance is destroyed."""
 65            logger.write_info_to_report("Stopping thermal chamber loop")
 66
 67    @property
 68    def air_temperature(self) -> float:
 69        """Get the current chamber temperature."""
 70        return float(self.resource.query("SOUR:CLO1:PVAL?"))
 71
 72    @property
 73    def set_point_temperature(self) -> float:
 74        """Get the set point temperature."""
 75        return float(self.resource.query("SOUR:CLO1:SPO?"))  # Set point/Closed-Loop Set Point, default 75.0°F
 76
 77    @set_point_temperature.setter
 78    def set_point_temperature(self, new_temp: float):
 79        """Apply new set point."""
 80        self.resource.write(f"SOUR:CLO1:SPO {new_temp}")  # Set point, default 75.0°F
 81
 82    def set_room_temperature(self):
 83        """Set the target temperature to room temperature."""
 84        old_units = self.internal_units
 85        self.internal_units = "C"
 86        self.set_point_temperature = 23.0  # Room temperature
 87        self.internal_units = old_units
 88
 89    def block_until_set_point_reached(self, timeout: float | None = None, period: float = 1, buffer: float = 1):
 90        """Wait until the thermal chamber has reached its set point
 91        :param timeout: raise TimeoutError after a certain amount of seconds has passed
 92        :param period: how often the temperature is checked in seconds
 93        :param buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
 94        """
 95        logger.write_debug_to_report(f"Blocking until {self.set_point_temperature} {self.internal_units}")
 96        start_time = time.perf_counter()
 97        while not self.set_point_temperature - buffer <= self.air_temperature <= self.set_point_temperature + buffer:
 98            if timeout is not None and time.perf_counter() - start_time >= timeout:
 99                raise TimeoutExceededError(f"Temperature was not reached after {timeout} seconds")
100            logger.write_debug_to_report(
101                f"Current temp: {self.air_temperature} - Target temp: {self.set_point_temperature} +- {buffer}"
102            )
103            time.sleep(period)
104
105    @property
106    def status(self) -> str:
107        """Read controller status."""
108        return "Constant"
109
110    def start_loop(self):
111        """Start loop 1 on chamber."""
112
113    def stop_loop(self):
114        """Stop loop 1 on chamber"""
115
116    @property
117    def date_and_time(self) -> datetime:
118        """Read current date on controller."""
119        return cast(datetime, datetime.now() - self._timedelta)
120
121    @date_and_time.setter
122    def date_and_time(self, new_date: datetime):
123        """Write new date to controller."""
124        self._timedelta = datetime.now() - new_date
125
126    @property
127    def internal_units(self) -> str:
128        """Read internal temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
129        return str(self.resource.query(":UNIT:TEMP?"))
130
131    @internal_units.setter
132    def internal_units(self, new_units: str):
133        """Write internal temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
134        self.resource.write(f":UNIT:TEMP {new_units.upper()}")
135
136    @property
137    def display_units(self) -> str:
138        """Read display temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
139        return str(self.resource.query(":UNIT:TEMP:DISP?"))
140
141    @display_units.setter
142    def display_units(self, new_units: str):
143        """Write display temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
144        self.resource.write(f":UNIT:TEMP:DISP {new_units.upper()}")
class ThermalChamber:
 46class ThermalChamber:
 47    """Watlow F4T controller command wrapper."""
 48
 49    def __init__(self):
 50        """Initialize the Watlow F4T controller."""
 51        if (
 52            hasattr(pytest, "flags")
 53            and isinstance(pytest.flags, BMSFlags)
 54            and "thermal_chamber_address" in pytest.flags.config
 55        ):
 56            logger.write_info_to_report("Connecting to thermal chamber...")
 57            ip = pytest.flags.config["thermal_chamber_address"]
 58            self.resource = SafeResource((f"TCPIP::{ip}::5025::SOCKET", "\n", "\n"))
 59            self.internal_units = "C"
 60            self.display_units = "C"
 61            self._timedelta = timedelta()
 62
 63        @atexit.register
 64        def __atexit__():
 65            """Configure a safe shut down for when the class instance is destroyed."""
 66            logger.write_info_to_report("Stopping thermal chamber loop")
 67
 68    @property
 69    def air_temperature(self) -> float:
 70        """Get the current chamber temperature."""
 71        return float(self.resource.query("SOUR:CLO1:PVAL?"))
 72
 73    @property
 74    def set_point_temperature(self) -> float:
 75        """Get the set point temperature."""
 76        return float(self.resource.query("SOUR:CLO1:SPO?"))  # Set point/Closed-Loop Set Point, default 75.0°F
 77
 78    @set_point_temperature.setter
 79    def set_point_temperature(self, new_temp: float):
 80        """Apply new set point."""
 81        self.resource.write(f"SOUR:CLO1:SPO {new_temp}")  # Set point, default 75.0°F
 82
 83    def set_room_temperature(self):
 84        """Set the target temperature to room temperature."""
 85        old_units = self.internal_units
 86        self.internal_units = "C"
 87        self.set_point_temperature = 23.0  # Room temperature
 88        self.internal_units = old_units
 89
 90    def block_until_set_point_reached(self, timeout: float | None = None, period: float = 1, buffer: float = 1):
 91        """Wait until the thermal chamber has reached its set point
 92        :param timeout: raise TimeoutError after a certain amount of seconds has passed
 93        :param period: how often the temperature is checked in seconds
 94        :param buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
 95        """
 96        logger.write_debug_to_report(f"Blocking until {self.set_point_temperature} {self.internal_units}")
 97        start_time = time.perf_counter()
 98        while not self.set_point_temperature - buffer <= self.air_temperature <= self.set_point_temperature + buffer:
 99            if timeout is not None and time.perf_counter() - start_time >= timeout:
100                raise TimeoutExceededError(f"Temperature was not reached after {timeout} seconds")
101            logger.write_debug_to_report(
102                f"Current temp: {self.air_temperature} - Target temp: {self.set_point_temperature} +- {buffer}"
103            )
104            time.sleep(period)
105
106    @property
107    def status(self) -> str:
108        """Read controller status."""
109        return "Constant"
110
111    def start_loop(self):
112        """Start loop 1 on chamber."""
113
114    def stop_loop(self):
115        """Stop loop 1 on chamber"""
116
117    @property
118    def date_and_time(self) -> datetime:
119        """Read current date on controller."""
120        return cast(datetime, datetime.now() - self._timedelta)
121
122    @date_and_time.setter
123    def date_and_time(self, new_date: datetime):
124        """Write new date to controller."""
125        self._timedelta = datetime.now() - new_date
126
127    @property
128    def internal_units(self) -> str:
129        """Read internal temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
130        return str(self.resource.query(":UNIT:TEMP?"))
131
132    @internal_units.setter
133    def internal_units(self, new_units: str):
134        """Write internal temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
135        self.resource.write(f":UNIT:TEMP {new_units.upper()}")
136
137    @property
138    def display_units(self) -> str:
139        """Read display temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
140        return str(self.resource.query(":UNIT:TEMP:DISP?"))
141
142    @display_units.setter
143    def display_units(self, new_units: str):
144        """Write display temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
145        self.resource.write(f":UNIT:TEMP:DISP {new_units.upper()}")

Watlow F4T controller command wrapper.

ThermalChamber()
49    def __init__(self):
50        """Initialize the Watlow F4T controller."""
51        if (
52            hasattr(pytest, "flags")
53            and isinstance(pytest.flags, BMSFlags)
54            and "thermal_chamber_address" in pytest.flags.config
55        ):
56            logger.write_info_to_report("Connecting to thermal chamber...")
57            ip = pytest.flags.config["thermal_chamber_address"]
58            self.resource = SafeResource((f"TCPIP::{ip}::5025::SOCKET", "\n", "\n"))
59            self.internal_units = "C"
60            self.display_units = "C"
61            self._timedelta = timedelta()
62
63        @atexit.register
64        def __atexit__():
65            """Configure a safe shut down for when the class instance is destroyed."""
66            logger.write_info_to_report("Stopping thermal chamber loop")

Initialize the Watlow F4T controller.

air_temperature: float
68    @property
69    def air_temperature(self) -> float:
70        """Get the current chamber temperature."""
71        return float(self.resource.query("SOUR:CLO1:PVAL?"))

Get the current chamber temperature.

set_point_temperature: float
73    @property
74    def set_point_temperature(self) -> float:
75        """Get the set point temperature."""
76        return float(self.resource.query("SOUR:CLO1:SPO?"))  # Set point/Closed-Loop Set Point, default 75.0°F

Get the set point temperature.

def set_room_temperature(self):
83    def set_room_temperature(self):
84        """Set the target temperature to room temperature."""
85        old_units = self.internal_units
86        self.internal_units = "C"
87        self.set_point_temperature = 23.0  # Room temperature
88        self.internal_units = old_units

Set the target temperature to room temperature.

def block_until_set_point_reached( self, timeout: float | None = None, period: float = 1, buffer: float = 1):
 90    def block_until_set_point_reached(self, timeout: float | None = None, period: float = 1, buffer: float = 1):
 91        """Wait until the thermal chamber has reached its set point
 92        :param timeout: raise TimeoutError after a certain amount of seconds has passed
 93        :param period: how often the temperature is checked in seconds
 94        :param buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
 95        """
 96        logger.write_debug_to_report(f"Blocking until {self.set_point_temperature} {self.internal_units}")
 97        start_time = time.perf_counter()
 98        while not self.set_point_temperature - buffer <= self.air_temperature <= self.set_point_temperature + buffer:
 99            if timeout is not None and time.perf_counter() - start_time >= timeout:
100                raise TimeoutExceededError(f"Temperature was not reached after {timeout} seconds")
101            logger.write_debug_to_report(
102                f"Current temp: {self.air_temperature} - Target temp: {self.set_point_temperature} +- {buffer}"
103            )
104            time.sleep(period)

Wait until the thermal chamber has reached its set point

Parameters
  • timeout: raise TimeoutError after a certain amount of seconds has passed
  • period: how often the temperature is checked in seconds
  • buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
status: str
106    @property
107    def status(self) -> str:
108        """Read controller status."""
109        return "Constant"

Read controller status.

def start_loop(self):
111    def start_loop(self):
112        """Start loop 1 on chamber."""

Start loop 1 on chamber.

def stop_loop(self):
114    def stop_loop(self):
115        """Stop loop 1 on chamber"""

Stop loop 1 on chamber

date_and_time: datetime.datetime
117    @property
118    def date_and_time(self) -> datetime:
119        """Read current date on controller."""
120        return cast(datetime, datetime.now() - self._timedelta)

Read current date on controller.

internal_units: str
127    @property
128    def internal_units(self) -> str:
129        """Read internal temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
130        return str(self.resource.query(":UNIT:TEMP?"))

Read internal temperature units ('C' for Celsius, 'F' for Fahrenheit).

display_units: str
137    @property
138    def display_units(self) -> str:
139        """Read display temperature units ('C' for Celsius, 'F' for Fahrenheit)."""
140        return str(self.resource.query(":UNIT:TEMP:DISP?"))

Read display temperature units ('C' for Celsius, 'F' for Fahrenheit).