hitl_tester.modules.bms.pseudo_hardware

This module provides simple simulations for the hardware found on the Raspberry Pi. This allows dry running test cases without owning or remoting into a HITL.

(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 module provides simple simulations for the hardware found on the Raspberry Pi.
  3This allows dry running test cases without owning or remoting into a HITL.
  4
  5# (c) 2020-2024 TurnAround Factor, Inc.
  6#
  7# CUI DISTRIBUTION CONTROL
  8# Controlled by: DLA J68 R&D SBIP
  9# CUI Category: Small Business Research and Technology
 10# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
 11# POC: GOV SBIP Program Manager Denise Price, 571-767-0111
 12# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
 13# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
 14# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
 15# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
 16# Fort Belvoir, VA 22060-6221
 17#
 18# SBIR DATA RIGHTS
 19# Contract No.:SP4701-23-C-0083
 20# Contractor Name: TurnAround Factor, Inc.
 21# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
 22# Expiration of SBIR Data Rights Period: September 24, 2029
 23# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
 24# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
 25# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
 26# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
 27# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
 28# the markings.
 29"""
 30
 31from __future__ import annotations
 32
 33import atexit
 34import random
 35import time
 36from datetime import datetime
 37
 38from hitl_tester.modules.bms_types import TimeoutExceededError
 39from hitl_tester.modules.bms.korad import Status
 40from hitl_tester.modules.logger import logger
 41
 42
 43# TODO(JA): would it be easier to simulate pyvisa for cell/load/dmm/charger/chamber.
 44# We would pass in a fake resource
 45
 46
 47class M300Dmm:
 48    """M300 Dmm."""
 49
 50    def __init__(self, scan_list: list[dict[str, int]]):
 51        self._volts = 1.4
 52        self.scan_list = scan_list
 53
 54    def configure_voltage_trigger(self):
 55        """Set up trigger parameters. Up to 10,000 readings."""
 56
 57    def send_trigger(self):
 58        """Send a software trigger."""
 59
 60    def read_internal_memory(self) -> list[float]:
 61        """Return measured data from internal memory."""
 62        return []
 63
 64    def configure_voltage_normal(self):
 65        """The voltage settings for normal measurements."""
 66
 67    @property
 68    def volts(self) -> float:
 69        """Measures DC voltage."""
 70        self._volts -= 0.001
 71        return float(self._volts)
 72
 73    @property
 74    def amps_ac(self) -> float:
 75        """Measures AC current."""
 76        return 1.0
 77
 78    def reset(self):
 79        """Resets the instrument."""
 80
 81
 82class ADC:
 83    """An ADC plate from PiPlates."""
 84
 85    VOLTAGE = 1.1
 86    MEASUREMENTS_TAKEN = 0
 87
 88    @staticmethod
 89    def setMODE(addr: int, mode: str):  # pylint: disable=unused-argument,invalid-name  # third-party library
 90        """Change mode of plate."""
 91
 92    @staticmethod
 93    def configINPUT(
 94        addr: int, channel: str, sample_rate: int, enable: bool
 95    ):  # pylint: disable=unused-argument,invalid-name  # third-party library
 96        """Configure plate input."""
 97
 98    @staticmethod
 99    def readSINGLE(  # pylint: disable=unused-argument,invalid-name  # From a third-party library
100        addr: int, channel: str
101    ) -> float | None:
102        """Read a plate channel."""
103        if ADC.MEASUREMENTS_TAKEN % 16 == 0:
104            ADC.VOLTAGE -= 0.0005
105        ADC.MEASUREMENTS_TAKEN -= 1
106        return ADC.VOLTAGE + (ADC.MEASUREMENTS_TAKEN % 16) * 0.01
107
108
109class RELAY2:
110    """A RELAY2 plate from PiPlates."""
111
112    @staticmethod
113    def relayON(board_id: int, channel_id: int):  # pylint: disable=unused-argument,invalid-name
114        """Activate relay."""
115
116    @staticmethod
117    def relayOFF(board_id: int, channel_id: int):  # pylint: disable=unused-argument,invalid-name
118        """Activate relay."""
119
120
121class Korad:
122    """Korad KA6003P power supply command wrapper."""
123
124    VOLTAGE_MAX = 60
125    VOLTAGE_MIN = 1.2
126    CURRENT_MAX = 3
127
128    def __init__(self, korad_id: int):
129        """
130        Initialize the KA6003P wrapper with a specific PyVISA resource.
131        This class does NOT open the resource, you have to open it for yourself!
132        """
133        self.id: int = korad_id
134        self._overvoltage_protection = False
135        self._overcurrent_protection = False
136        self._beep = False
137        self._volts = 3.0
138        self._amps = 1.0
139
140        @atexit.register
141        def __atexit__():
142            """Configure a safe shut down for when the class instance is destroyed."""
143            self.disable()
144
145    @property
146    def status(self) -> Status:
147        """Get the power supply status."""
148        return Status(Status.OUTPUT_ON | Status.OVERVOLTAGE_OVERCURRENT_PROTECTION)
149
150    def set_profile(self, volts: float, amps: float):
151        """Sets charging profile"""
152        self.volts = volts
153        self.amps = amps
154
155    @property
156    def overcurrent_protection(self) -> float:
157        """Get the last set overcurrent protection state."""
158        return self._overcurrent_protection
159
160    @overcurrent_protection.setter
161    def overcurrent_protection(self, enabled: bool):
162        """Enable or disable overcurrent protection."""
163        self._overcurrent_protection = enabled
164
165    @property
166    def overvoltage_protection(self) -> float:
167        """Get the last set overvoltage protection state."""
168        return self._overvoltage_protection
169
170    @overvoltage_protection.setter
171    def overvoltage_protection(self, enabled: bool):
172        """Enable or disable overvoltage protection."""
173
174    @property
175    def measured_amps(self) -> float:
176        """Measures current."""
177        return self._amps
178
179    @property
180    def amps(self) -> float:
181        """Get the target current."""
182        return self._amps
183
184    @amps.setter
185    def amps(self, new_amps: float):
186        """Set the target current."""
187        if new_amps > Korad.CURRENT_MAX:
188            raise RuntimeError(f"Current of {new_amps}A exceeds maximum of {Korad.CURRENT_MAX}A.")
189        self._amps = new_amps
190
191    @property
192    def measured_volts(self) -> float:
193        """Measures voltage."""
194        return self._volts
195
196    @property
197    def volts(self) -> float:
198        """Get the target voltage."""
199        return self._volts
200
201    @volts.setter
202    def volts(self, new_volts: float):
203        """Set the target voltage"""
204        if not Korad.VOLTAGE_MIN <= new_volts <= Korad.VOLTAGE_MAX:
205            raise RuntimeError(f"{new_volts}V is out of range {Korad.VOLTAGE_MIN}V to {Korad.VOLTAGE_MAX}V.")
206        self._volts = new_volts
207
208    @property
209    def beep(self) -> bool:
210        """Get the beep setting."""
211        return self._beep
212
213    @beep.setter
214    def beep(self, enable_beep: bool):
215        """Turns on or off the beep."""
216
217    def recall(self, memory_number: int):
218        """Recalls a panel setting from memory 1 to 5."""
219
220    def store(self, memory_number: int):
221        """Stores a panel setting to memory 1 to 5."""
222
223    def enable(self):
224        """Enable the output."""
225
226    def disable(self):
227        """Disable the output."""
228
229
230class Cell:
231    """Basic Agilent 66321 Battery Simulator simulator."""
232
233    def __init__(self, cell_id, cell_chemistry):  # pylint: disable=unused-argument
234        self.id = cell_id
235        self.volts = 0
236        self.ohms = 0.030
237        self.amps = 0
238        self.compensation_mode = None
239        self.state_of_charge = 0
240        self.measured_volts = 0
241
242        @atexit.register
243        def __atexit__():
244            """Configure a safe shut down for when the class instance is destroyed."""
245            logger.write_info_to_report(f"Disabling cell {self.id}")
246            self.disable()
247
248    def enable(self):
249        """Enables power supply output"""
250
251    def disable(self):
252        """Enables power supply output"""
253
254    def reset(self):
255        """Resets the instrument"""
256
257    def volts_to_soc(self, voltage):
258        """Convert voltage to soc based on temp."""
259        return (voltage or 5) / 10
260
261
262class Charger:
263    """Basic Rigol DL711 Power Supply simulator."""
264
265    def __init__(self):
266        self.volts = 0
267        self._amps = 2
268        self._amps_counter = 0
269        self.amps_limit = 0
270        self.volts_limit = 0
271        self.target_amps = 0
272        self.target_volts = 0
273
274        @atexit.register
275        def __atexit__():
276            """Configure a safe shut down for when the class instance is destroyed."""
277            self.disable()
278
279    @property
280    def amps(self):
281        """Simulate amps dropping"""
282        # self._amps_counter += 1
283        # self._amps -= 0.1
284        # if self._amps_counter in (3, 8, 9):
285        #     return 0.084
286        return 0 if 0.100 > self._amps > 0.021 else self._amps
287
288    def set_profile(self, volts, amps):
289        """Sets charging profile"""
290        self.target_volts = volts
291        self.target_amps = amps
292        self.volts = volts
293        self._amps = amps
294
295    def enable(self):
296        """Enables power supply output"""
297        logger.write_info_to_report("Enabling charger")
298
299    def disable(self):
300        """Disables power supply output"""
301        logger.write_info_to_report("Disabling charger")
302
303    def reset(self):
304        """Resets the instrument"""
305
306
307class Load:
308    """Basic Rigol DL3000 Electronic Load simulator."""
309
310    def __init__(self):
311        self.volts = 3.0
312        self.amps = 2.0
313        self.ohms = 10
314        self.amps_range = 0
315        self.volts_range = 0
316        self.ohms_r_range = 0
317        self.ohms_v_range = 0
318        self.ohms_i_range = 0
319
320        @atexit.register
321        def __atexit__():
322            """Configure a safe shut down for when the class instance is destroyed."""
323            self.disable()
324
325    def configure_pulse_trigger(self, pulse_current: int = 1, base_current: int = 0, duration: float = 0.25):
326        """Set up pulse trigger values."""
327
328    def wait_for_pulse(self):
329        """Wait for load pulse to complete."""
330
331    def send_trigger(self):
332        """Send a software trigger."""
333
334    def mode_cc(self):
335        """Sets load to constant current mode."""
336
337    def mode_cr(self):
338        """Sets load to constant resistance mode."""
339
340    def mode_cv(self):
341        """Sets load to constant voltage mode"""
342
343    def enable(self):
344        """Enables the load input"""
345        logger.write_info_to_report("Enabling load")
346
347    def disable(self):
348        """Disables the load input"""
349        logger.write_info_to_report("Disabling load")
350
351    def reset(self):
352        """Resets the instrument"""
353
354
355class Dmm:
356    """Basic Rigol DM3068 DMM simulator."""
357
358    def __init__(self):
359        self._helper = 0
360        self._volts = 4
361        self._amps_ac = 2
362        self._sample_count = 2000
363
364    def configure_voltage_trigger(self, sample_count: int = 2000):
365        """Set up trigger parameters."""
366        self._sample_count = min(2000, sample_count)
367
368    def send_trigger(self):
369        """Send a software trigger."""
370
371    def read_internal_memory(self) -> list[float]:
372        """Return measured data from internal memory."""
373        sample_data = [
374            3.37148454,
375            3.3714377,
376            3.36943853,
377            3.34266015,
378            3.33230059,
379            3.32955,
380            3.32771727,
381            3.32534937,
382            3.32193206,
383            3.32062852,
384        ]
385        for i in range(995):  # Add some noise to the first half of the measurements
386            sample_data.insert(0, sample_data[i] + random.random() / 10000 * random.choice((-1, 1)))
387        for _ in range(995):  # Decrease linearly
388            sample_data.append(sample_data[-1] - 0.0000309725902335)  # Magic number comes from real measurements
389        return sample_data[: self._sample_count]
390
391    @property
392    def volts(self) -> float:
393        """The voltage from the DMM"""
394        # self._helper += 1
395        # if False and self._helper >= 70:  # Trigger undervoltage after 50 readings
396        #     self._volts = 2.3
397        # else:
398        # self._volts = max(2.5, self._volts - 0.05)
399        return float(self._volts)
400
401    @property
402    def amps_ac(self) -> float:
403        """Measures AC current."""
404        return float(self._amps_ac)
405
406    def reset(self):
407        """Resets the instrument"""
408
409
410class THERMO:
411    """Basic thermocouple simulator"""
412
413    @staticmethod
414    def getTEMP(board_id, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
415        """Get the temperature as a float"""
416        return 24.0
417
418    @staticmethod
419    def getADDR(board_id):  # pylint: disable=invalid-name  # From a third-party library
420        """Returns board_id if board is present."""
421        return board_id
422
423
424class DAQC2:
425    """Basic Pi-Plates DAQC2 Plate simulator"""
426
427    @staticmethod
428    def setDAC(address, channel, vdac):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
429        """Set Digital-to-Analogue converter."""
430
431    @staticmethod
432    def setDOUTbit(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
433        """Set data out bit."""
434
435    @staticmethod
436    def clrDOUTbit(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
437        """Clear data out bit."""
438
439    @staticmethod
440    def getADC(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
441        """Temperature in celsius / 100."""
442        return 24 / 100
443
444
445class ThermalChamber:
446    """Watlow F4T controller command wrapper."""
447
448    def __init__(self) -> None:  # pylint: disable=unused-argument  # Dummy function
449        """Initialize the Watlow F4T controller."""
450        self._display_units = "F"
451        self._internal_units = "F"
452        self.date = datetime.today()
453        self._air_temperature = 26.0
454        self._set_point_temperature = 24.0
455
456        @atexit.register
457        def __atexit__():
458            """Configure a safe shut down for when the class instance is destroyed."""
459            logger.write_info_to_report("Stopping thermal chamber loop")
460            self.stop_loop()
461
462    @property
463    def air_temperature(self) -> float:
464        """Get process value
465        :returns: the current chamber temperature
466        """
467        try:
468            self._air_temperature += (
469                (self._set_point_temperature - self._air_temperature)
470                / abs(self._set_point_temperature - self._air_temperature)
471                / 2
472            )
473        except ZeroDivisionError:
474            pass
475        return self._air_temperature  # Working Process Value, reg 2820 [CHAMBER TEMPERATURE]
476
477    @property
478    def set_point_temperature(self) -> float:
479        """Get set point
480        :returns: the set point temperature
481        """
482        return float(self._set_point_temperature)  # Set point/Closed-Loop Set Point, reg 2782/2810, default 75.0°F
483
484    @set_point_temperature.setter
485    def set_point_temperature(self, new_temp: float):
486        """Apply new set point
487        :param new_temp: the new set point
488        """
489        self._set_point_temperature = new_temp  # Set point, reg 2782, default 75.0°F
490
491    def set_room_temperature(self):
492        """Set the target temperature to room temperature"""
493        old_units = self.internal_units
494        self.internal_units = "C"
495        self.set_point_temperature = 23.0  # Room temperature
496        self.internal_units = old_units
497
498    def block_until_set_point_reached(self, timeout: float | None = None, period: float = 1, buffer: float = 1):
499        """Wait until the thermal chamber has reached its set point
500        :param timeout: raise TimeoutError after a certain amount of seconds has passed
501        :param period: how often the temperature is checked in seconds
502        :param buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
503        """
504        logger.write_debug_to_report(f"Blocking until {self.set_point_temperature} {self.internal_units}")
505        start_time = time.perf_counter()
506        while not self.set_point_temperature - buffer <= self.air_temperature <= self.set_point_temperature + buffer:
507            if timeout is not None and time.perf_counter() - start_time >= timeout:
508                raise TimeoutExceededError(f"Temperature was not reached after {timeout} seconds")
509            logger.write_debug_to_report(
510                f"Current temp: {self.air_temperature} - Target temp: {self.set_point_temperature} +- {buffer}"
511            )
512            time.sleep(period)
513
514    @property
515    def status(self) -> str:
516        """Read controller status
517        :returns: the chamber status
518        """
519        return "Constant"
520
521    def start_loop(self):
522        """Start loop 1 on chamber"""
523
524    def stop_loop(self):
525        """Stop loop 1 on chamber"""
526
527    @property
528    def date_and_time(self) -> datetime:
529        """Read current date on controller"""
530        return self.date
531
532    @date_and_time.setter
533    def date_and_time(self, new_date: datetime):
534        """Write new date to controller
535        :param new_date: the date to set the controller to
536        """
537        self.date = new_date
538
539    @property
540    def internal_units(self) -> str:
541        """Read internal temperature units
542        :returns: 'C' for Celsius, 'F' for Fahrenheit
543        """
544        return self._internal_units
545
546    @internal_units.setter
547    def internal_units(self, new_units: str):
548        """Write internal temperature units
549        :param new_units: 'C' for Celsius, 'F' for Fahrenheit
550        """
551        self._internal_units = new_units
552
553    @property
554    def display_units(self) -> str:
555        """Read display temperature units
556        :returns: 'C' for Celsius, 'F' for Fahrenheit
557        """
558        return self._display_units
559
560    @display_units.setter
561    def display_units(self, new_units: str):
562        """Write display temperature units
563        :param new_units: 'C' for Celsius, 'F' for Fahrenheit
564        """
565        self._display_units = new_units
class M300Dmm:
48class M300Dmm:
49    """M300 Dmm."""
50
51    def __init__(self, scan_list: list[dict[str, int]]):
52        self._volts = 1.4
53        self.scan_list = scan_list
54
55    def configure_voltage_trigger(self):
56        """Set up trigger parameters. Up to 10,000 readings."""
57
58    def send_trigger(self):
59        """Send a software trigger."""
60
61    def read_internal_memory(self) -> list[float]:
62        """Return measured data from internal memory."""
63        return []
64
65    def configure_voltage_normal(self):
66        """The voltage settings for normal measurements."""
67
68    @property
69    def volts(self) -> float:
70        """Measures DC voltage."""
71        self._volts -= 0.001
72        return float(self._volts)
73
74    @property
75    def amps_ac(self) -> float:
76        """Measures AC current."""
77        return 1.0
78
79    def reset(self):
80        """Resets the instrument."""

M300 Dmm.

M300Dmm(scan_list: list[dict[str, int]])
51    def __init__(self, scan_list: list[dict[str, int]]):
52        self._volts = 1.4
53        self.scan_list = scan_list
scan_list
def configure_voltage_trigger(self):
55    def configure_voltage_trigger(self):
56        """Set up trigger parameters. Up to 10,000 readings."""

Set up trigger parameters. Up to 10,000 readings.

def send_trigger(self):
58    def send_trigger(self):
59        """Send a software trigger."""

Send a software trigger.

def read_internal_memory(self) -> list[float]:
61    def read_internal_memory(self) -> list[float]:
62        """Return measured data from internal memory."""
63        return []

Return measured data from internal memory.

def configure_voltage_normal(self):
65    def configure_voltage_normal(self):
66        """The voltage settings for normal measurements."""

The voltage settings for normal measurements.

volts: float
68    @property
69    def volts(self) -> float:
70        """Measures DC voltage."""
71        self._volts -= 0.001
72        return float(self._volts)

Measures DC voltage.

amps_ac: float
74    @property
75    def amps_ac(self) -> float:
76        """Measures AC current."""
77        return 1.0

Measures AC current.

def reset(self):
79    def reset(self):
80        """Resets the instrument."""

Resets the instrument.

class ADC:
 83class ADC:
 84    """An ADC plate from PiPlates."""
 85
 86    VOLTAGE = 1.1
 87    MEASUREMENTS_TAKEN = 0
 88
 89    @staticmethod
 90    def setMODE(addr: int, mode: str):  # pylint: disable=unused-argument,invalid-name  # third-party library
 91        """Change mode of plate."""
 92
 93    @staticmethod
 94    def configINPUT(
 95        addr: int, channel: str, sample_rate: int, enable: bool
 96    ):  # pylint: disable=unused-argument,invalid-name  # third-party library
 97        """Configure plate input."""
 98
 99    @staticmethod
100    def readSINGLE(  # pylint: disable=unused-argument,invalid-name  # From a third-party library
101        addr: int, channel: str
102    ) -> float | None:
103        """Read a plate channel."""
104        if ADC.MEASUREMENTS_TAKEN % 16 == 0:
105            ADC.VOLTAGE -= 0.0005
106        ADC.MEASUREMENTS_TAKEN -= 1
107        return ADC.VOLTAGE + (ADC.MEASUREMENTS_TAKEN % 16) * 0.01

An ADC plate from PiPlates.

VOLTAGE = 1.1
MEASUREMENTS_TAKEN = 0
@staticmethod
def setMODE(addr: int, mode: str):
89    @staticmethod
90    def setMODE(addr: int, mode: str):  # pylint: disable=unused-argument,invalid-name  # third-party library
91        """Change mode of plate."""

Change mode of plate.

@staticmethod
def configINPUT(addr: int, channel: str, sample_rate: int, enable: bool):
93    @staticmethod
94    def configINPUT(
95        addr: int, channel: str, sample_rate: int, enable: bool
96    ):  # pylint: disable=unused-argument,invalid-name  # third-party library
97        """Configure plate input."""

Configure plate input.

@staticmethod
def readSINGLE(addr: int, channel: str) -> float | None:
 99    @staticmethod
100    def readSINGLE(  # pylint: disable=unused-argument,invalid-name  # From a third-party library
101        addr: int, channel: str
102    ) -> float | None:
103        """Read a plate channel."""
104        if ADC.MEASUREMENTS_TAKEN % 16 == 0:
105            ADC.VOLTAGE -= 0.0005
106        ADC.MEASUREMENTS_TAKEN -= 1
107        return ADC.VOLTAGE + (ADC.MEASUREMENTS_TAKEN % 16) * 0.01

Read a plate channel.

class RELAY2:
110class RELAY2:
111    """A RELAY2 plate from PiPlates."""
112
113    @staticmethod
114    def relayON(board_id: int, channel_id: int):  # pylint: disable=unused-argument,invalid-name
115        """Activate relay."""
116
117    @staticmethod
118    def relayOFF(board_id: int, channel_id: int):  # pylint: disable=unused-argument,invalid-name
119        """Activate relay."""

A RELAY2 plate from PiPlates.

@staticmethod
def relayON(board_id: int, channel_id: int):
113    @staticmethod
114    def relayON(board_id: int, channel_id: int):  # pylint: disable=unused-argument,invalid-name
115        """Activate relay."""

Activate relay.

@staticmethod
def relayOFF(board_id: int, channel_id: int):
117    @staticmethod
118    def relayOFF(board_id: int, channel_id: int):  # pylint: disable=unused-argument,invalid-name
119        """Activate relay."""

Activate relay.

class Korad:
122class Korad:
123    """Korad KA6003P power supply command wrapper."""
124
125    VOLTAGE_MAX = 60
126    VOLTAGE_MIN = 1.2
127    CURRENT_MAX = 3
128
129    def __init__(self, korad_id: int):
130        """
131        Initialize the KA6003P wrapper with a specific PyVISA resource.
132        This class does NOT open the resource, you have to open it for yourself!
133        """
134        self.id: int = korad_id
135        self._overvoltage_protection = False
136        self._overcurrent_protection = False
137        self._beep = False
138        self._volts = 3.0
139        self._amps = 1.0
140
141        @atexit.register
142        def __atexit__():
143            """Configure a safe shut down for when the class instance is destroyed."""
144            self.disable()
145
146    @property
147    def status(self) -> Status:
148        """Get the power supply status."""
149        return Status(Status.OUTPUT_ON | Status.OVERVOLTAGE_OVERCURRENT_PROTECTION)
150
151    def set_profile(self, volts: float, amps: float):
152        """Sets charging profile"""
153        self.volts = volts
154        self.amps = amps
155
156    @property
157    def overcurrent_protection(self) -> float:
158        """Get the last set overcurrent protection state."""
159        return self._overcurrent_protection
160
161    @overcurrent_protection.setter
162    def overcurrent_protection(self, enabled: bool):
163        """Enable or disable overcurrent protection."""
164        self._overcurrent_protection = enabled
165
166    @property
167    def overvoltage_protection(self) -> float:
168        """Get the last set overvoltage protection state."""
169        return self._overvoltage_protection
170
171    @overvoltage_protection.setter
172    def overvoltage_protection(self, enabled: bool):
173        """Enable or disable overvoltage protection."""
174
175    @property
176    def measured_amps(self) -> float:
177        """Measures current."""
178        return self._amps
179
180    @property
181    def amps(self) -> float:
182        """Get the target current."""
183        return self._amps
184
185    @amps.setter
186    def amps(self, new_amps: float):
187        """Set the target current."""
188        if new_amps > Korad.CURRENT_MAX:
189            raise RuntimeError(f"Current of {new_amps}A exceeds maximum of {Korad.CURRENT_MAX}A.")
190        self._amps = new_amps
191
192    @property
193    def measured_volts(self) -> float:
194        """Measures voltage."""
195        return self._volts
196
197    @property
198    def volts(self) -> float:
199        """Get the target voltage."""
200        return self._volts
201
202    @volts.setter
203    def volts(self, new_volts: float):
204        """Set the target voltage"""
205        if not Korad.VOLTAGE_MIN <= new_volts <= Korad.VOLTAGE_MAX:
206            raise RuntimeError(f"{new_volts}V is out of range {Korad.VOLTAGE_MIN}V to {Korad.VOLTAGE_MAX}V.")
207        self._volts = new_volts
208
209    @property
210    def beep(self) -> bool:
211        """Get the beep setting."""
212        return self._beep
213
214    @beep.setter
215    def beep(self, enable_beep: bool):
216        """Turns on or off the beep."""
217
218    def recall(self, memory_number: int):
219        """Recalls a panel setting from memory 1 to 5."""
220
221    def store(self, memory_number: int):
222        """Stores a panel setting to memory 1 to 5."""
223
224    def enable(self):
225        """Enable the output."""
226
227    def disable(self):
228        """Disable the output."""

Korad KA6003P power supply command wrapper.

Korad(korad_id: int)
129    def __init__(self, korad_id: int):
130        """
131        Initialize the KA6003P wrapper with a specific PyVISA resource.
132        This class does NOT open the resource, you have to open it for yourself!
133        """
134        self.id: int = korad_id
135        self._overvoltage_protection = False
136        self._overcurrent_protection = False
137        self._beep = False
138        self._volts = 3.0
139        self._amps = 1.0
140
141        @atexit.register
142        def __atexit__():
143            """Configure a safe shut down for when the class instance is destroyed."""
144            self.disable()

Initialize the KA6003P wrapper with a specific PyVISA resource. This class does NOT open the resource, you have to open it for yourself!

VOLTAGE_MAX = 60
VOLTAGE_MIN = 1.2
CURRENT_MAX = 3
id: int
146    @property
147    def status(self) -> Status:
148        """Get the power supply status."""
149        return Status(Status.OUTPUT_ON | Status.OVERVOLTAGE_OVERCURRENT_PROTECTION)

Get the power supply status.

def set_profile(self, volts: float, amps: float):
151    def set_profile(self, volts: float, amps: float):
152        """Sets charging profile"""
153        self.volts = volts
154        self.amps = amps

Sets charging profile

overcurrent_protection: float
156    @property
157    def overcurrent_protection(self) -> float:
158        """Get the last set overcurrent protection state."""
159        return self._overcurrent_protection

Get the last set overcurrent protection state.

overvoltage_protection: float
166    @property
167    def overvoltage_protection(self) -> float:
168        """Get the last set overvoltage protection state."""
169        return self._overvoltage_protection

Get the last set overvoltage protection state.

measured_amps: float
175    @property
176    def measured_amps(self) -> float:
177        """Measures current."""
178        return self._amps

Measures current.

amps: float
180    @property
181    def amps(self) -> float:
182        """Get the target current."""
183        return self._amps

Get the target current.

measured_volts: float
192    @property
193    def measured_volts(self) -> float:
194        """Measures voltage."""
195        return self._volts

Measures voltage.

volts: float
197    @property
198    def volts(self) -> float:
199        """Get the target voltage."""
200        return self._volts

Get the target voltage.

beep: bool
209    @property
210    def beep(self) -> bool:
211        """Get the beep setting."""
212        return self._beep

Get the beep setting.

def recall(self, memory_number: int):
218    def recall(self, memory_number: int):
219        """Recalls a panel setting from memory 1 to 5."""

Recalls a panel setting from memory 1 to 5.

def store(self, memory_number: int):
221    def store(self, memory_number: int):
222        """Stores a panel setting to memory 1 to 5."""

Stores a panel setting to memory 1 to 5.

def enable(self):
224    def enable(self):
225        """Enable the output."""

Enable the output.

def disable(self):
227    def disable(self):
228        """Disable the output."""

Disable the output.

class Cell:
231class Cell:
232    """Basic Agilent 66321 Battery Simulator simulator."""
233
234    def __init__(self, cell_id, cell_chemistry):  # pylint: disable=unused-argument
235        self.id = cell_id
236        self.volts = 0
237        self.ohms = 0.030
238        self.amps = 0
239        self.compensation_mode = None
240        self.state_of_charge = 0
241        self.measured_volts = 0
242
243        @atexit.register
244        def __atexit__():
245            """Configure a safe shut down for when the class instance is destroyed."""
246            logger.write_info_to_report(f"Disabling cell {self.id}")
247            self.disable()
248
249    def enable(self):
250        """Enables power supply output"""
251
252    def disable(self):
253        """Enables power supply output"""
254
255    def reset(self):
256        """Resets the instrument"""
257
258    def volts_to_soc(self, voltage):
259        """Convert voltage to soc based on temp."""
260        return (voltage or 5) / 10

Basic Agilent 66321 Battery Simulator simulator.

Cell(cell_id, cell_chemistry)
234    def __init__(self, cell_id, cell_chemistry):  # pylint: disable=unused-argument
235        self.id = cell_id
236        self.volts = 0
237        self.ohms = 0.030
238        self.amps = 0
239        self.compensation_mode = None
240        self.state_of_charge = 0
241        self.measured_volts = 0
242
243        @atexit.register
244        def __atexit__():
245            """Configure a safe shut down for when the class instance is destroyed."""
246            logger.write_info_to_report(f"Disabling cell {self.id}")
247            self.disable()
id
volts
ohms
amps
compensation_mode
state_of_charge
measured_volts
def enable(self):
249    def enable(self):
250        """Enables power supply output"""

Enables power supply output

def disable(self):
252    def disable(self):
253        """Enables power supply output"""

Enables power supply output

def reset(self):
255    def reset(self):
256        """Resets the instrument"""

Resets the instrument

def volts_to_soc(self, voltage):
258    def volts_to_soc(self, voltage):
259        """Convert voltage to soc based on temp."""
260        return (voltage or 5) / 10

Convert voltage to soc based on temp.

class Charger:
263class Charger:
264    """Basic Rigol DL711 Power Supply simulator."""
265
266    def __init__(self):
267        self.volts = 0
268        self._amps = 2
269        self._amps_counter = 0
270        self.amps_limit = 0
271        self.volts_limit = 0
272        self.target_amps = 0
273        self.target_volts = 0
274
275        @atexit.register
276        def __atexit__():
277            """Configure a safe shut down for when the class instance is destroyed."""
278            self.disable()
279
280    @property
281    def amps(self):
282        """Simulate amps dropping"""
283        # self._amps_counter += 1
284        # self._amps -= 0.1
285        # if self._amps_counter in (3, 8, 9):
286        #     return 0.084
287        return 0 if 0.100 > self._amps > 0.021 else self._amps
288
289    def set_profile(self, volts, amps):
290        """Sets charging profile"""
291        self.target_volts = volts
292        self.target_amps = amps
293        self.volts = volts
294        self._amps = amps
295
296    def enable(self):
297        """Enables power supply output"""
298        logger.write_info_to_report("Enabling charger")
299
300    def disable(self):
301        """Disables power supply output"""
302        logger.write_info_to_report("Disabling charger")
303
304    def reset(self):
305        """Resets the instrument"""

Basic Rigol DL711 Power Supply simulator.

volts
amps_limit
volts_limit
target_amps
target_volts
amps
280    @property
281    def amps(self):
282        """Simulate amps dropping"""
283        # self._amps_counter += 1
284        # self._amps -= 0.1
285        # if self._amps_counter in (3, 8, 9):
286        #     return 0.084
287        return 0 if 0.100 > self._amps > 0.021 else self._amps

Simulate amps dropping

def set_profile(self, volts, amps):
289    def set_profile(self, volts, amps):
290        """Sets charging profile"""
291        self.target_volts = volts
292        self.target_amps = amps
293        self.volts = volts
294        self._amps = amps

Sets charging profile

def enable(self):
296    def enable(self):
297        """Enables power supply output"""
298        logger.write_info_to_report("Enabling charger")

Enables power supply output

def disable(self):
300    def disable(self):
301        """Disables power supply output"""
302        logger.write_info_to_report("Disabling charger")

Disables power supply output

def reset(self):
304    def reset(self):
305        """Resets the instrument"""

Resets the instrument

class Load:
308class Load:
309    """Basic Rigol DL3000 Electronic Load simulator."""
310
311    def __init__(self):
312        self.volts = 3.0
313        self.amps = 2.0
314        self.ohms = 10
315        self.amps_range = 0
316        self.volts_range = 0
317        self.ohms_r_range = 0
318        self.ohms_v_range = 0
319        self.ohms_i_range = 0
320
321        @atexit.register
322        def __atexit__():
323            """Configure a safe shut down for when the class instance is destroyed."""
324            self.disable()
325
326    def configure_pulse_trigger(self, pulse_current: int = 1, base_current: int = 0, duration: float = 0.25):
327        """Set up pulse trigger values."""
328
329    def wait_for_pulse(self):
330        """Wait for load pulse to complete."""
331
332    def send_trigger(self):
333        """Send a software trigger."""
334
335    def mode_cc(self):
336        """Sets load to constant current mode."""
337
338    def mode_cr(self):
339        """Sets load to constant resistance mode."""
340
341    def mode_cv(self):
342        """Sets load to constant voltage mode"""
343
344    def enable(self):
345        """Enables the load input"""
346        logger.write_info_to_report("Enabling load")
347
348    def disable(self):
349        """Disables the load input"""
350        logger.write_info_to_report("Disabling load")
351
352    def reset(self):
353        """Resets the instrument"""

Basic Rigol DL3000 Electronic Load simulator.

volts
amps
ohms
amps_range
volts_range
ohms_r_range
ohms_v_range
ohms_i_range
def configure_pulse_trigger( self, pulse_current: int = 1, base_current: int = 0, duration: float = 0.25):
326    def configure_pulse_trigger(self, pulse_current: int = 1, base_current: int = 0, duration: float = 0.25):
327        """Set up pulse trigger values."""

Set up pulse trigger values.

def wait_for_pulse(self):
329    def wait_for_pulse(self):
330        """Wait for load pulse to complete."""

Wait for load pulse to complete.

def send_trigger(self):
332    def send_trigger(self):
333        """Send a software trigger."""

Send a software trigger.

def mode_cc(self):
335    def mode_cc(self):
336        """Sets load to constant current mode."""

Sets load to constant current mode.

def mode_cr(self):
338    def mode_cr(self):
339        """Sets load to constant resistance mode."""

Sets load to constant resistance mode.

def mode_cv(self):
341    def mode_cv(self):
342        """Sets load to constant voltage mode"""

Sets load to constant voltage mode

def enable(self):
344    def enable(self):
345        """Enables the load input"""
346        logger.write_info_to_report("Enabling load")

Enables the load input

def disable(self):
348    def disable(self):
349        """Disables the load input"""
350        logger.write_info_to_report("Disabling load")

Disables the load input

def reset(self):
352    def reset(self):
353        """Resets the instrument"""

Resets the instrument

class Dmm:
356class Dmm:
357    """Basic Rigol DM3068 DMM simulator."""
358
359    def __init__(self):
360        self._helper = 0
361        self._volts = 4
362        self._amps_ac = 2
363        self._sample_count = 2000
364
365    def configure_voltage_trigger(self, sample_count: int = 2000):
366        """Set up trigger parameters."""
367        self._sample_count = min(2000, sample_count)
368
369    def send_trigger(self):
370        """Send a software trigger."""
371
372    def read_internal_memory(self) -> list[float]:
373        """Return measured data from internal memory."""
374        sample_data = [
375            3.37148454,
376            3.3714377,
377            3.36943853,
378            3.34266015,
379            3.33230059,
380            3.32955,
381            3.32771727,
382            3.32534937,
383            3.32193206,
384            3.32062852,
385        ]
386        for i in range(995):  # Add some noise to the first half of the measurements
387            sample_data.insert(0, sample_data[i] + random.random() / 10000 * random.choice((-1, 1)))
388        for _ in range(995):  # Decrease linearly
389            sample_data.append(sample_data[-1] - 0.0000309725902335)  # Magic number comes from real measurements
390        return sample_data[: self._sample_count]
391
392    @property
393    def volts(self) -> float:
394        """The voltage from the DMM"""
395        # self._helper += 1
396        # if False and self._helper >= 70:  # Trigger undervoltage after 50 readings
397        #     self._volts = 2.3
398        # else:
399        # self._volts = max(2.5, self._volts - 0.05)
400        return float(self._volts)
401
402    @property
403    def amps_ac(self) -> float:
404        """Measures AC current."""
405        return float(self._amps_ac)
406
407    def reset(self):
408        """Resets the instrument"""

Basic Rigol DM3068 DMM simulator.

def configure_voltage_trigger(self, sample_count: int = 2000):
365    def configure_voltage_trigger(self, sample_count: int = 2000):
366        """Set up trigger parameters."""
367        self._sample_count = min(2000, sample_count)

Set up trigger parameters.

def send_trigger(self):
369    def send_trigger(self):
370        """Send a software trigger."""

Send a software trigger.

def read_internal_memory(self) -> list[float]:
372    def read_internal_memory(self) -> list[float]:
373        """Return measured data from internal memory."""
374        sample_data = [
375            3.37148454,
376            3.3714377,
377            3.36943853,
378            3.34266015,
379            3.33230059,
380            3.32955,
381            3.32771727,
382            3.32534937,
383            3.32193206,
384            3.32062852,
385        ]
386        for i in range(995):  # Add some noise to the first half of the measurements
387            sample_data.insert(0, sample_data[i] + random.random() / 10000 * random.choice((-1, 1)))
388        for _ in range(995):  # Decrease linearly
389            sample_data.append(sample_data[-1] - 0.0000309725902335)  # Magic number comes from real measurements
390        return sample_data[: self._sample_count]

Return measured data from internal memory.

volts: float
392    @property
393    def volts(self) -> float:
394        """The voltage from the DMM"""
395        # self._helper += 1
396        # if False and self._helper >= 70:  # Trigger undervoltage after 50 readings
397        #     self._volts = 2.3
398        # else:
399        # self._volts = max(2.5, self._volts - 0.05)
400        return float(self._volts)

The voltage from the DMM

amps_ac: float
402    @property
403    def amps_ac(self) -> float:
404        """Measures AC current."""
405        return float(self._amps_ac)

Measures AC current.

def reset(self):
407    def reset(self):
408        """Resets the instrument"""

Resets the instrument

class THERMO:
411class THERMO:
412    """Basic thermocouple simulator"""
413
414    @staticmethod
415    def getTEMP(board_id, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
416        """Get the temperature as a float"""
417        return 24.0
418
419    @staticmethod
420    def getADDR(board_id):  # pylint: disable=invalid-name  # From a third-party library
421        """Returns board_id if board is present."""
422        return board_id

Basic thermocouple simulator

@staticmethod
def getTEMP(board_id, channel):
414    @staticmethod
415    def getTEMP(board_id, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
416        """Get the temperature as a float"""
417        return 24.0

Get the temperature as a float

@staticmethod
def getADDR(board_id):
419    @staticmethod
420    def getADDR(board_id):  # pylint: disable=invalid-name  # From a third-party library
421        """Returns board_id if board is present."""
422        return board_id

Returns board_id if board is present.

class DAQC2:
425class DAQC2:
426    """Basic Pi-Plates DAQC2 Plate simulator"""
427
428    @staticmethod
429    def setDAC(address, channel, vdac):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
430        """Set Digital-to-Analogue converter."""
431
432    @staticmethod
433    def setDOUTbit(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
434        """Set data out bit."""
435
436    @staticmethod
437    def clrDOUTbit(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
438        """Clear data out bit."""
439
440    @staticmethod
441    def getADC(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
442        """Temperature in celsius / 100."""
443        return 24 / 100

Basic Pi-Plates DAQC2 Plate simulator

@staticmethod
def setDAC(address, channel, vdac):
428    @staticmethod
429    def setDAC(address, channel, vdac):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
430        """Set Digital-to-Analogue converter."""

Set Digital-to-Analogue converter.

@staticmethod
def setDOUTbit(address, channel):
432    @staticmethod
433    def setDOUTbit(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
434        """Set data out bit."""

Set data out bit.

@staticmethod
def clrDOUTbit(address, channel):
436    @staticmethod
437    def clrDOUTbit(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
438        """Clear data out bit."""

Clear data out bit.

@staticmethod
def getADC(address, channel):
440    @staticmethod
441    def getADC(address, channel):  # pylint: disable=unused-argument,invalid-name  # From a third-party library
442        """Temperature in celsius / 100."""
443        return 24 / 100

Temperature in celsius / 100.

class ThermalChamber:
446class ThermalChamber:
447    """Watlow F4T controller command wrapper."""
448
449    def __init__(self) -> None:  # pylint: disable=unused-argument  # Dummy function
450        """Initialize the Watlow F4T controller."""
451        self._display_units = "F"
452        self._internal_units = "F"
453        self.date = datetime.today()
454        self._air_temperature = 26.0
455        self._set_point_temperature = 24.0
456
457        @atexit.register
458        def __atexit__():
459            """Configure a safe shut down for when the class instance is destroyed."""
460            logger.write_info_to_report("Stopping thermal chamber loop")
461            self.stop_loop()
462
463    @property
464    def air_temperature(self) -> float:
465        """Get process value
466        :returns: the current chamber temperature
467        """
468        try:
469            self._air_temperature += (
470                (self._set_point_temperature - self._air_temperature)
471                / abs(self._set_point_temperature - self._air_temperature)
472                / 2
473            )
474        except ZeroDivisionError:
475            pass
476        return self._air_temperature  # Working Process Value, reg 2820 [CHAMBER TEMPERATURE]
477
478    @property
479    def set_point_temperature(self) -> float:
480        """Get set point
481        :returns: the set point temperature
482        """
483        return float(self._set_point_temperature)  # Set point/Closed-Loop Set Point, reg 2782/2810, default 75.0°F
484
485    @set_point_temperature.setter
486    def set_point_temperature(self, new_temp: float):
487        """Apply new set point
488        :param new_temp: the new set point
489        """
490        self._set_point_temperature = new_temp  # Set point, reg 2782, default 75.0°F
491
492    def set_room_temperature(self):
493        """Set the target temperature to room temperature"""
494        old_units = self.internal_units
495        self.internal_units = "C"
496        self.set_point_temperature = 23.0  # Room temperature
497        self.internal_units = old_units
498
499    def block_until_set_point_reached(self, timeout: float | None = None, period: float = 1, buffer: float = 1):
500        """Wait until the thermal chamber has reached its set point
501        :param timeout: raise TimeoutError after a certain amount of seconds has passed
502        :param period: how often the temperature is checked in seconds
503        :param buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
504        """
505        logger.write_debug_to_report(f"Blocking until {self.set_point_temperature} {self.internal_units}")
506        start_time = time.perf_counter()
507        while not self.set_point_temperature - buffer <= self.air_temperature <= self.set_point_temperature + buffer:
508            if timeout is not None and time.perf_counter() - start_time >= timeout:
509                raise TimeoutExceededError(f"Temperature was not reached after {timeout} seconds")
510            logger.write_debug_to_report(
511                f"Current temp: {self.air_temperature} - Target temp: {self.set_point_temperature} +- {buffer}"
512            )
513            time.sleep(period)
514
515    @property
516    def status(self) -> str:
517        """Read controller status
518        :returns: the chamber status
519        """
520        return "Constant"
521
522    def start_loop(self):
523        """Start loop 1 on chamber"""
524
525    def stop_loop(self):
526        """Stop loop 1 on chamber"""
527
528    @property
529    def date_and_time(self) -> datetime:
530        """Read current date on controller"""
531        return self.date
532
533    @date_and_time.setter
534    def date_and_time(self, new_date: datetime):
535        """Write new date to controller
536        :param new_date: the date to set the controller to
537        """
538        self.date = new_date
539
540    @property
541    def internal_units(self) -> str:
542        """Read internal temperature units
543        :returns: 'C' for Celsius, 'F' for Fahrenheit
544        """
545        return self._internal_units
546
547    @internal_units.setter
548    def internal_units(self, new_units: str):
549        """Write internal temperature units
550        :param new_units: 'C' for Celsius, 'F' for Fahrenheit
551        """
552        self._internal_units = new_units
553
554    @property
555    def display_units(self) -> str:
556        """Read display temperature units
557        :returns: 'C' for Celsius, 'F' for Fahrenheit
558        """
559        return self._display_units
560
561    @display_units.setter
562    def display_units(self, new_units: str):
563        """Write display temperature units
564        :param new_units: 'C' for Celsius, 'F' for Fahrenheit
565        """
566        self._display_units = new_units

Watlow F4T controller command wrapper.

ThermalChamber()
449    def __init__(self) -> None:  # pylint: disable=unused-argument  # Dummy function
450        """Initialize the Watlow F4T controller."""
451        self._display_units = "F"
452        self._internal_units = "F"
453        self.date = datetime.today()
454        self._air_temperature = 26.0
455        self._set_point_temperature = 24.0
456
457        @atexit.register
458        def __atexit__():
459            """Configure a safe shut down for when the class instance is destroyed."""
460            logger.write_info_to_report("Stopping thermal chamber loop")
461            self.stop_loop()

Initialize the Watlow F4T controller.

date
air_temperature: float
463    @property
464    def air_temperature(self) -> float:
465        """Get process value
466        :returns: the current chamber temperature
467        """
468        try:
469            self._air_temperature += (
470                (self._set_point_temperature - self._air_temperature)
471                / abs(self._set_point_temperature - self._air_temperature)
472                / 2
473            )
474        except ZeroDivisionError:
475            pass
476        return self._air_temperature  # Working Process Value, reg 2820 [CHAMBER TEMPERATURE]

Get process value :returns: the current chamber temperature

set_point_temperature: float
478    @property
479    def set_point_temperature(self) -> float:
480        """Get set point
481        :returns: the set point temperature
482        """
483        return float(self._set_point_temperature)  # Set point/Closed-Loop Set Point, reg 2782/2810, default 75.0°F

Get set point :returns: the set point temperature

def set_room_temperature(self):
492    def set_room_temperature(self):
493        """Set the target temperature to room temperature"""
494        old_units = self.internal_units
495        self.internal_units = "C"
496        self.set_point_temperature = 23.0  # Room temperature
497        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):
499    def block_until_set_point_reached(self, timeout: float | None = None, period: float = 1, buffer: float = 1):
500        """Wait until the thermal chamber has reached its set point
501        :param timeout: raise TimeoutError after a certain amount of seconds has passed
502        :param period: how often the temperature is checked in seconds
503        :param buffer: Since measured temperature may not be precise, we actually wait until set_point +- buffer
504        """
505        logger.write_debug_to_report(f"Blocking until {self.set_point_temperature} {self.internal_units}")
506        start_time = time.perf_counter()
507        while not self.set_point_temperature - buffer <= self.air_temperature <= self.set_point_temperature + buffer:
508            if timeout is not None and time.perf_counter() - start_time >= timeout:
509                raise TimeoutExceededError(f"Temperature was not reached after {timeout} seconds")
510            logger.write_debug_to_report(
511                f"Current temp: {self.air_temperature} - Target temp: {self.set_point_temperature} +- {buffer}"
512            )
513            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
515    @property
516    def status(self) -> str:
517        """Read controller status
518        :returns: the chamber status
519        """
520        return "Constant"

Read controller status :returns: the chamber status

def start_loop(self):
522    def start_loop(self):
523        """Start loop 1 on chamber"""

Start loop 1 on chamber

def stop_loop(self):
525    def stop_loop(self):
526        """Stop loop 1 on chamber"""

Stop loop 1 on chamber

date_and_time: datetime.datetime
528    @property
529    def date_and_time(self) -> datetime:
530        """Read current date on controller"""
531        return self.date

Read current date on controller

internal_units: str
540    @property
541    def internal_units(self) -> str:
542        """Read internal temperature units
543        :returns: 'C' for Celsius, 'F' for Fahrenheit
544        """
545        return self._internal_units

Read internal temperature units :returns: 'C' for Celsius, 'F' for Fahrenheit

display_units: str
554    @property
555    def display_units(self) -> str:
556        """Read display temperature units
557        :returns: 'C' for Celsius, 'F' for Fahrenheit
558        """
559        return self._display_units

Read display temperature units :returns: 'C' for Celsius, 'F' for Fahrenheit