Skip to content

tc4820

frog.hardware.plugins.temperature.tc4820 ¤

This module provides an interface to TC4820 temperature controllers.

Decimal numbers are used for values sent to and read from the device as the values are base-10 and using floats could cause rounding errors.

There are broadly two serial-related exceptions that are raised by this module. MalformedMessageErrors are raised when a message is corrupted and are recoverable (i.e. you can try submitting the request again). serial.SerialExceptions indicate that an IO error occurred while communicating with the device (e.g. because a USB cable has become disconnected) and are unlikely to be recoverable. A SerialException is also raised if multiple attempts at a request have failed.

Classes¤

MalformedMessageError ¤

Bases: Exception

Raised when a message sent or received was malformed.

TC4820(name, port, baudrate=115200, max_attempts=3) ¤

Bases: SerialDevice, TemperatureControllerBase

An interface for TC4820 temperature controllers.

Create a new TC4820 from an existing serial device.

Parameters:

Name Type Description Default
name str

The name of the device, to distinguish it from others

required
port str

Description of USB port (vendor ID + product ID)

required
baudrate int

Baud rate of port

115200
max_attempts int

Maximum number of attempts for requests

3
Source code in frog/hardware/plugins/temperature/tc4820.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def __init__(
    self, name: str, port: str, baudrate: int = 115200, max_attempts: int = 3
) -> None:
    """Create a new TC4820 from an existing serial device.

    Args:
        name: The name of the device, to distinguish it from others
        port: Description of USB port (vendor ID + product ID)
        baudrate: Baud rate of port
        max_attempts: Maximum number of attempts for requests
    """
    if max_attempts < 1:
        raise ValueError("max_attempts must be at least 1")

    self.max_attempts = max_attempts

    SerialDevice.__init__(self, port, baudrate)
    TemperatureControllerBase.__init__(self, name)
Attributes¤
alarm_status property ¤

The current error status of the system.

A value of zero indicates that no error has occurred.

TODO: Figure out what the error codes mean

power property ¤

The current power output of the device, as a percentage of maximum.

set_point property writable ¤

The set point temperature (in degrees).

In other words, this indicates the temperature the device is aiming towards.

temperature property ¤

The current temperature reported by the device.

Functions¤
checksum(message) staticmethod ¤

Calculate a checksum for a message sent or received.

Source code in frog/hardware/plugins/temperature/tc4820.py
205
206
207
208
209
@staticmethod
def checksum(message: str) -> str:
    """Calculate a checksum for a message sent or received."""
    csum = sum(message.encode("ascii")) & 0xFF
    return f"{csum:0{2}x}"
close() ¤

Close the device.

Source code in frog/hardware/plugins/temperature/tc4820.py
55
56
57
58
def close(self) -> None:
    """Close the device."""
    TemperatureControllerBase.close(self)
    SerialDevice.close(self)
read_int() ¤

Read a message from the TC4820 and decode the number as a signed integer.

Valid messages have the form "*{number}{checksum}^", where {number} is a signed integer represented as a zero-padded four-char hexadecimal number and {checksum} is the checksum for this message, represented as a zero-padded two-char hexadecimal number (see checksum() function for details). Negative numbers are represented as if they had been cast to an unsigned integer before being encoded as hex (i.e. -1 is represented as "ffff").

There is one special message "*XXXX60^", which is what the device sends when the checksum for the last message we sent didn't match.

If we receive a malformed message or "*XXXX60^", a MalformedMessageError is raised. A SerialException can also be raised by the underlying PySerial library, which indicates that a lower-level IO error has occurred (e.g. because the USB cable has become disconnected).

Raises:

Type Description
MalformedMessageError

The read message was malformed or the device is complaining that our message was malformed

SerialException

An error occurred while reading the device

Source code in frog/hardware/plugins/temperature/tc4820.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def read_int(self) -> int:
    """Read a message from the TC4820 and decode the number as a signed integer.

    Valid messages have the form "*{number}{checksum}^", where {number} is a signed
    integer represented as a zero-padded four-char hexadecimal number and {checksum}
    is the checksum for this message, represented as a zero-padded two-char
    hexadecimal number (see checksum() function for details). Negative numbers are
    represented as if they had been cast to an unsigned integer before being encoded
    as hex (i.e. -1 is represented as "ffff").

    There is one special message "*XXXX60^", which is what the device sends when the
    checksum for the last message we sent didn't match.

    If we receive a malformed message or "*XXXX60^", a MalformedMessageError is
    raised. A SerialException can also be raised by the underlying PySerial library,
    which indicates that a lower-level IO error has occurred (e.g. because the USB
    cable has become disconnected).

    Raises:
        MalformedMessageError: The read message was malformed or the device is
                               complaining that our message was malformed
        SerialException: An error occurred while reading the device
    """
    message_bytes = self.serial.read_until(b"^", size=8)

    # Don't handle decoding errors, because these will be caught by bytes.fromhex()
    # below
    message = message_bytes.decode("ascii", errors="replace")

    if len(message) != 8 or message[0] != "*" or message[-1] != "^":
        raise MalformedMessageError(f"Malformed message received: {message}")

    if message == "*XXXX60^":
        raise MalformedMessageError("Bad checksum sent")

    if message[5:7] != self.checksum(message[1:5]):
        raise MalformedMessageError("Bad checksum received")

    try:
        # Turn the hex string into raw bytes...
        int_bytes = bytes.fromhex(message[1:5])
    except ValueError as e:
        raise MalformedMessageError("Number was not provided as hex") from e

    # ...then convert the raw bytes to a signed int
    return int.from_bytes(int_bytes, byteorder="big", signed=True)
request_decimal(command) ¤

Write the specified command then read a Decimal from the device.

If the request fails because of a checksum failure, then retransmission will be attempted a maximum of self.max_attempts times.

Raises:

Type Description
SerialException

An error occurred while communicating with the device or max attempts was exceeded

Source code in frog/hardware/plugins/temperature/tc4820.py
149
150
151
152
153
154
155
156
157
158
159
def request_decimal(self, command: str) -> Decimal:
    """Write the specified command then read a Decimal from the device.

    If the request fails because of a checksum failure, then retransmission will be
    attempted a maximum of self.max_attempts times.

    Raises:
        SerialException: An error occurred while communicating with the device or
                         max attempts was exceeded
    """
    return self.to_decimal(self.request_int(command))
request_int(command) ¤

Write the specified command then read an int from the device.

If the request fails because a malformed message was received or the device indicates that our message was corrupted, then retransmission will be attempted a maximum of self.max_attempts times.

Raises:

Type Description
SerialException

An error occurred while communicating with the device or max attempts was exceeded

Source code in frog/hardware/plugins/temperature/tc4820.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def request_int(self, command: str) -> int:
    """Write the specified command then read an int from the device.

    If the request fails because a malformed message was received or the device
    indicates that our message was corrupted, then retransmission will be attempted
    a maximum of self.max_attempts times.

    Raises:
        SerialException: An error occurred while communicating with the device or
                         max attempts was exceeded
    """
    for _ in range(self.max_attempts):
        self.send_command(command)

        try:
            return self.read_int()
        except MalformedMessageError as e:
            logging.warn(f"Malformed message: {e!s}; retrying")

    raise SerialException(
        f"Maximum number of attempts (={self.max_attempts}) exceeded"
    )
send_command(command) ¤

Write a message to the TC4820.

The command is usually an integer represented as a zero-padded six-char hexadecimal string.

Sent messages are encoded similarly (but not identically) to those received and look like "*{command}{checksum}\r", where the checksum is calculated as it is for received messages.

Parameters:

Name Type Description Default
command str

The string command to send

required

Raises: SerialException: An error occurred while writing to the device

Source code in frog/hardware/plugins/temperature/tc4820.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def send_command(self, command: str) -> None:
    r"""Write a message to the TC4820.

    The command is usually an integer represented as a zero-padded six-char
    hexadecimal string.

    Sent messages are encoded similarly (but not identically) to those received and
    look like "*{command}{checksum}\r", where the checksum is calculated as it is
    for received messages.

    Args:
        command: The string command to send
    Raises:
        SerialException: An error occurred while writing to the device
    """
    checksum = self.checksum(command)
    message = f"*{command}{checksum}\r"
    self.serial.write(message.encode("ascii"))
to_decimal(value) staticmethod ¤

Convert an int from the TC4820 to a Decimal.

Source code in frog/hardware/plugins/temperature/tc4820.py
211
212
213
214
215
@staticmethod
def to_decimal(value: int) -> Decimal:
    """Convert an int from the TC4820 to a Decimal."""
    # Decimal values are encoded as 10x their value then converted to an int.
    return Decimal(value) / Decimal(10)