-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
1216ea2
commit 96169c2
Showing
11 changed files
with
570 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ Contents: | |
fluke | ||
gentec-eo | ||
glassman | ||
hcp | ||
holzworth | ||
hp | ||
keithley | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.