Source code for instruments.hcp.tc038d
#!/usr/bin/env python
"""
Provides support for the TC038 AC crystal oven by HC Photonics.
"""
# IMPORTS #####################################################################
from instruments.units import ureg as u
from instruments.abstract_instruments.instrument import Instrument
from instruments.util_fns import assume_units
# CLASSES #####################################################################
[docs]
class TC038D(Instrument):
"""
Communication with the HCP TC038D oven.
This is the newer version with DC heating.
The temperature controller is on default set to modbus communication.
The oven expects raw bytes written, no ascii code, and sends raw bytes.
For the variables are two or four-byte modes available. We use the
four-byte mode addresses, so do we. In that case element count has to be
double the variables read.
"""
functions = {"read": 0x03, "writeMultiple": 0x10, "writeSingle": 0x06, "echo": 0x08}
byteMode = 4
def __init__(self, *args, **kwargs):
"""
The TC038 is a crystal oven.
Example usage:
>>> import instruments as ik
>>> import instruments.units as u
>>> inst = ik.hcp.TC038.open_serial('COM10')
>>> inst.setpoint = 45.3
>>> print(inst.temperature)
"""
super().__init__(*args, **kwargs)
self.addr = 1
[docs]
@staticmethod
def CRC16(data):
"""Calculate the CRC16 checksum for the data byte array."""
CRC = 0xFFFF
for octet in data:
CRC ^= octet
for j in range(8):
lsb = CRC & 0x1 # least significant bit
CRC = CRC >> 1
if lsb:
CRC ^= 0xA001
return [CRC & 0xFF, CRC >> 8]
[docs]
def readRegister(self, address, count=1):
"""Read count variables from start address on."""
# Count has to be double the number of elements in 4-byte-mode.
count *= self.byteMode // 2
data = [self.addr]
data.append(self.functions["read"]) # function code
data += [address >> 8, address & 0xFF] # 2B address
data += [count >> 8, count & 0xFF] # 2B number of elements
data += self.CRC16(data)
self._file.write_raw(bytes(data))
# Slave address, function, length
got = self.read_raw(3)
if got[1] == self.functions["read"]:
length = got[2]
# data length, 2 Byte CRC
read = self.read_raw(length + 2)
if read[-2:] != bytes(self.CRC16(got + read[:-2])):
raise ConnectionError("Response CRC does not match.")
return read[:-2]
else: # an error occurred
end = self.read_raw(2) # empty the buffer
if got[2] == 0x02:
raise ValueError("The read start address is incorrect.")
if got[2] == 0x03:
raise ValueError("The number of elements exceeds the allowed range")
raise ConnectionError(f"Unknown read error. Received: {got} {end}")
[docs]
def writeMultiple(self, address, values):
"""Write multiple variables."""
data = [self.addr]
data.append(self.functions["writeMultiple"]) # function code
data += [address >> 8, address & 0xFF] # 2B address
if isinstance(values, int):
data += [0x0, self.byteMode // 2] # 2B number of elements
data.append(self.byteMode) # 1B number of write data
for i in range(self.byteMode - 1, -1, -1):
data.append(values >> i * 8 & 0xFF)
elif hasattr(values, "__iter__"):
elements = len(values) * self.byteMode // 2
data += [elements >> 8, elements & 0xFF] # 2B number of elements
data.append(len(values) * self.byteMode) # 1B number of write data
for element in values:
for i in range(self.byteMode - 1, -1, -1):
data.append(element >> i * 8 & 0xFF)
else:
raise ValueError(
"Values has to be an integer or an iterable of "
f"integers. values: {values}"
)
data += self.CRC16(data)
self._file.write_raw(bytes(data))
got = self.read_raw(2)
# slave address, function
if got[1] == self.functions["writeMultiple"]:
# start address, number elements, CRC; each 2 Bytes long
got += self.read_raw(2 + 2 + 2)
if got[-2:] != bytes(self.CRC16(got[:-2])):
raise ConnectionError("Response CRC does not match.")
else:
end = self.read_raw(3) # error code and CRC
errors = {
0x02: "Wrong start address",
0x03: "Variable data error",
0x04: "Operation error",
}
raise ValueError(errors[end[0]])
@property
def setpoint(self):
"""Get the current setpoint in °C."""
value = int.from_bytes(self.readRegister(0x106), byteorder="big") / 10
return u.Quantity(value, u.degC)
@setpoint.setter
def setpoint(self, value):
"""Set the setpoint in °C."""
number = assume_units(value, u.degC).to(u.degC).magnitude
value = int(round(value.to("degC").magnitude * 10, 0))
self.writeMultiple(0x106, int(round(number * 10)))
@property
def temperature(self):
"""Get the current temperature in °C."""
value = int.from_bytes(self.readRegister(0x0), byteorder="big") / 10
return u.Quantity(value, u.degC)