Skip to content

Commit

Permalink
Hcp ovens added (#356)
Browse files Browse the repository at this point in the history
* HCP TC038, TC038D crystal ovenss added.

Proper value and unit handling.
Tests added.

* TC038 monitored value made more clear. Tests added.

* Several improvements of robustness.

CRC check for TC038D (including tests).
More and better tests.
Typo in TC038.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tests added.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Documentation added and slight improvements TC038.

Documentation files added.
monitored_quantity is a property now.
Connection settings are set in init.
Tests adjusted accordingly.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Reordered parity.

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
BenediktBurger and pre-commit-ci[bot] committed Jul 13, 2022
1 parent 1216ea2 commit 96169c2
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 0 deletions.
22 changes: 22 additions & 0 deletions doc/source/apiref/hcp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
..
TODO: put documentation license header here.
.. currentmodule:: instruments.hcp

============
HC Photonics
============

:class:`TC038` Crystal oven AC
==============================

.. autoclass:: TC038
:members:
:undoc-members:

:class:`TC038D` Crystal oven DC
===============================

.. autoclass:: TC038D
:members:
:undoc-members:
1 change: 1 addition & 0 deletions doc/source/apiref/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Contents:
fluke
gentec-eo
glassman
hcp
holzworth
hp
keithley
Expand Down
1 change: 1 addition & 0 deletions src/instruments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from . import fluke
from . import gentec_eo
from . import glassman
from . import hcp
from . import holzworth
from . import hp
from . import keithley
Expand Down
13 changes: 13 additions & 0 deletions src/instruments/abstract_instruments/comm/serial_communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ def timeout(self, newval):
newval = assume_units(newval, u.second).to(u.second).magnitude
self._conn.timeout = newval

@property
def parity(self):
"""
Gets / sets the communication parity.
:type: `str`
"""
return self._conn.parity

@parity.setter
def parity(self, newval):
self._conn.parity = newval

# FILE-LIKE METHODS #

def close(self):
Expand Down
8 changes: 8 additions & 0 deletions src/instruments/hcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env python
"""
Module containing HC Photonics instruments
"""


from .tc038 import TC038
from .tc038d import TC038D
146 changes: 146 additions & 0 deletions src/instruments/hcp/tc038.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/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 #####################################################################


class TC038(Instrument):
"""
Communication with the HCP TC038 oven.
This is the older version with an AC power supply and AC heater.
It has parity or framing errors from time to time. Handle them in your
application.
"""

_registers = {
"temperature": "D0002",
"setpoint": "D0120",
}

def __init__(self, *args, **kwargs):
"""
Initialize 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.terminator = "\r"
self.addr = 1
self._monitored_quantity = None
self._file.parity = "E" # serial.PARITY_EVEN

def sendcmd(self, command):
"""
Send "command" to the oven with "commandData".
Parameters
----------
command : string, optional
Command to be sent. Three chars indicating the type, and data for
the command, if necessary.
"""
# 010 is CPU (01) and time to wait (0), which are fix
super().sendcmd(chr(2) + f"{self.addr:02}" + "010" + command + chr(3))

def query(self, command):
"""
Send a command to the oven and read its response.
Parameters
----------
command : string, optional
Command to be sent. Three chars indicating the type, and data for
the command, if necessary.
Returns
-------
string
response of the system.
"""
return super().query(chr(2) + f"{self.addr:02}" + "010" + command + chr(3))

@property
def monitored_quantity(self):
"""The monitored quantity."""
return self._monitored_quantity

@monitored_quantity.setter
def monitored_quantity(self, quantity="temperature"):
"""
Configure the oven to monitor a certain `quantity`.
`quantity` may be any key of `_registers`. Default is the current
temperature in °C.
"""
assert quantity in self._registers.keys(), f"Quantity {quantity} is unknown."
# WRS in order to setup to monitor a word
# monitor 1 to 16 words
# monitor the word in the given register
# Additional registers are added with a separating space or comma.
self.query(command="WRS" + "01" + self._registers[quantity])
self._monitored_quantity = quantity

@property
def setpoint(self):
"""Read and return the current setpoint in °C."""
got = self.query(command="WRD" + "D0120" + ",01")
# WRD: read words
# start with register D0003
# read a single word, separated by space or comma
return self._data_to_temp(got)

@setpoint.setter
def setpoint(self, value):
"""Set the setpoint to a temperature in °C."""
number = assume_units(value, u.degC).to(u.degC).magnitude
commandData = f"D0120,01,{int(round(number * 10)):04X}"
# Temperature without decimal sign in hex representation
got = self.query(command="WWR" + commandData)
assert got[5:7] == "OK", "A communication error occurred."

@property
def temperature(self):
"""Read and return the current temperature in °C."""
got = self.query(command="WRD" + "D0002" + ",01")
return self._data_to_temp(got)

@property
def monitored_value(self):
"""
Read and return the monitored value.
Per default it's the current temperature in °C.
"""
# returns the monitored words
got = self.query(command="WRM")
return self._data_to_temp(got)

@property
def information(self):
"""Read the device information."""
return self.query("INF6")[7:-1]

@staticmethod
def _data_to_temp(data):
"""Convert the returned hex value "data" to a temperature in °C."""
return u.Quantity(int(data[7:11], 16) / 10, u.degC)
# get the hex number, convert to int and shift the decimal sign
148 changes: 148 additions & 0 deletions src/instruments/hcp/tc038d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/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 #####################################################################


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

@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]

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}")

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)
10 changes: 10 additions & 0 deletions tests/test_comm/test_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ def test_serialcomm_timeout():
timeout.assert_called_with(1)


def test_serialcomm_parity():
comm = SerialCommunicator(serial.Serial())

# Default parity should be NONE
assert comm.parity == serial.PARITY_NONE

comm.parity = serial.PARITY_EVEN
assert comm.parity == serial.PARITY_EVEN


def test_serialcomm_close():
comm = SerialCommunicator(serial.Serial())
comm._conn = mock.MagicMock()
Expand Down
Empty file added tests/test_hcp/__init__.py
Empty file.

0 comments on commit 96169c2

Please sign in to comment.