-
Notifications
You must be signed in to change notification settings - Fork 71
/
tc200.py
351 lines (287 loc) · 9.79 KB
/
tc200.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Provides the support for the Thorlabs TC200 temperature controller.
Class originally contributed by Catherine Holloway.
"""
# IMPORTS #####################################################################
from __future__ import absolute_import
from __future__ import division
from builtins import range, map
from enum import IntEnum, Enum
import quantities as pq
from instruments.abstract_instruments import Instrument
from instruments.util_fns import convert_temperature
from instruments.util_fns import enum_property, unitful_property, int_property
# CLASSES #####################################################################
class TC200(Instrument):
"""
The TC200 is is a controller for the voltage across a heating element.
It can also read in the temperature off of a thermistor and implements
a PID control to keep the temperature at a set value.
The user manual can be found here:
http://www.thorlabs.com/thorcat/12500/TC200-Manual.pdf
"""
def __init__(self, filelike):
super(TC200, self).__init__(filelike)
self.terminator = "\r"
self.prompt = "> "
def _ack_expected(self, msg=""):
return msg
# ENUMS #
class Mode(IntEnum):
"""
Enum containing valid output modes of the TC200.
"""
normal = 0
cycle = 1
class Sensor(Enum):
"""
Enum containing valid temperature sensor types for the TC200.
"""
ptc100 = "ptc100"
ptc1000 = "ptc1000"
th10k = "th10k"
ntc10k = "ntc10k"
# PROPERTIES #
def name(self):
"""
Gets the name and version number of the device
:return: the name string of the device
:rtype: str
"""
response = self.query("*idn?")
return response
@property
def mode(self):
"""
Gets/sets the output mode of the TC200
:type: `TC200.Mode`
"""
response = self.status
response_code = (int(response) >> 1) % 2
return TC200.Mode(response_code)
@mode.setter
def mode(self, newval):
if not isinstance(newval, TC200.Mode):
raise TypeError("Mode setting must be a `TC200.Mode` value, "
"got {} instead.".format(type(newval)))
out_query = "mode={}".format(newval.name)
# there is an issue with the TC200; it responds with a spurious
# Command Error on mode=normal. Thus, the sendcmd() method cannot
# be used.
if newval == TC200.Mode.normal:
self.prompt = "Command error CMD_ARG_RANGE_ERR\n\r> "
self.sendcmd(out_query)
self.prompt = "> "
else:
self.sendcmd(out_query)
@property
def enable(self):
"""
Gets/sets the heater enable status.
If output enable is on (`True`), there is a voltage on the output.
:type: `bool`
"""
response = self.status
return True if int(response) % 2 == 1 else False
@enable.setter
def enable(self, newval):
if not isinstance(newval, bool):
raise TypeError("TC200 enable property must be specified with a "
"boolean.")
# the "ens" command is a toggle, we need to track two different cases,
# when it should be on and it is off, and when it is off and
# should be on
# if no sensor is attached, the unit will respond with an error.
# There is no current error handling in the way that thorlabs
# responds with errors
if newval and not self.enable:
response1 = self._file.query("ens")
while response1 != ">":
response1 = self._file.read(1)
self._file.read(1)
elif not newval and self.enable:
response1 = self._file.query("ens")
while response1 != ">":
response1 = self._file.read(1)
self._file.read(1)
@property
def status(self):
"""
Gets the the status code of the TC200
:rtype: `int`
"""
_ = self._file.query(str("stat?"))
response = self.read(5)
return int(response.split(" ")[0])
temperature = unitful_property(
"tact",
units=pq.degC,
readonly=True,
input_decoration=lambda x: x.replace(
" C", "").replace(" F", "").replace(" K", ""),
doc="""
Gets the actual temperature of the sensor
:units: As specified (if a `~quantities.quantity.Quantity`) or assumed
to be of units degrees C.
:type: `~quantities.quantity.Quantity` or `int`
:return: the temperature (in degrees C)
:rtype: `~quantities.quantity.Quantity`
"""
)
max_temperature = unitful_property(
"tmax",
units=pq.degC,
format_code="{:.1f}",
set_fmt="{}={}",
valid_range=(20*pq.degC, 205*pq.degC),
doc="""
Gets/sets the maximum temperature
:return: the maximum temperature (in deg C)
:units: As specified or assumed to be degree Celsius. Returns with
units degC.
:rtype: `~quantities.quantity.Quantity`
"""
)
@property
def temperature_set(self):
"""
Gets/sets the actual temperature of the sensor
:units: As specified (if a `~quantities.quantity.Quantity`) or assumed
to be of units degrees C.
:type: `~quantities.quantity.Quantity` or `int`
:return: the temperature (in degrees C)
:rtype: `~quantities.quantity.Quantity`
"""
response = self.query("tset?").replace(
" C", "").replace(" F", "").replace(" K", "")
return float(response) * pq.degC
@temperature_set.setter
def temperature_set(self, newval):
# the set temperature is always in celsius
newval = convert_temperature(newval, pq.degC).magnitude
if newval < 20.0 or newval > self.max_temperature:
raise ValueError("Temperature set is out of range.")
out_query = "tset={}".format(newval)
self.sendcmd(out_query)
@property
def p(self):
"""
Gets/sets the p-gain. Valid numbers are [1,250].
:return: the p-gain (in nnn)
:rtype: `int`
"""
return self.pid[0]
@p.setter
def p(self, newval):
if newval not in range(1, 251):
raise ValueError("P-value not in [1, 250]")
self.sendcmd("pgain={}".format(newval))
@property
def i(self):
"""
Gets/sets the i-gain. Valid numbers are [1,250]
:return: the i-gain (in nnn)
:rtype: `int`
"""
return self.pid[1]
@i.setter
def i(self, newval):
if newval not in range(0, 251):
raise ValueError("I-value not in [0, 250]")
self.sendcmd("igain={}".format(newval))
@property
def d(self):
"""
Gets/sets the d-gain. Valid numbers are [0, 250]
:return: the d-gain (in nnn)
:type: `int`
"""
return self.pid[2]
@d.setter
def d(self, newval):
if newval not in range(0, 251):
raise ValueError("D-value not in [0, 250]")
self.sendcmd("dgain={}".format(newval))
@property
def pid(self):
"""
Gets/sets all three PID values at the same time. See `TC200.p`,
`TC200.i`, and `TC200.d` for individual restrictions.
If `None` is specified then the corresponding PID value is not changed.
:return: List of integers of PID values. In order [P, I, D].
:type: `list` or `tuple`
:rtype: `list`
"""
return list(map(int, self.query("pid?").split()))
@pid.setter
def pid(self, newval):
if not isinstance(newval, (list, tuple)):
raise TypeError("Setting PID must be specified as a list or tuple")
if newval[0] is not None:
self.p = newval[0]
if newval[1] is not None:
self.i = newval[1]
if newval[2] is not None:
self.d = newval[2]
@property
def degrees(self):
"""
Gets/sets the units of the temperature measurement.
:return: The temperature units (degC/F/K) the TC200 is measuring in
:type: `~quantities.unitquantity.UnitTemperature`
"""
response = self.status
if (response >> 4) % 2 and (response >> 5) % 2:
return pq.degC
elif (response >> 5) % 2:
return pq.degK
return pq.degF
@degrees.setter
def degrees(self, newval):
if newval is pq.degC:
self.sendcmd("unit=c")
elif newval is pq.degF:
self.sendcmd("unit=f")
elif newval is pq.degK:
self.sendcmd("unit=k")
else:
raise TypeError("Invalid temperature type")
sensor = enum_property(
"sns",
Sensor,
input_decoration=lambda x: x.split(
",")[0].split("=")[1].strip().lower(),
set_fmt="{}={}",
doc="""
Gets/sets the current thermistor type. Used for converting resistances
to temperatures.
:return: The thermistor type
:type: `TC200.Sensor`
"""
)
beta = int_property(
"beta",
valid_set=range(2000, 6001),
set_fmt="{}={}",
doc="""
Gets/sets the beta value of the thermistor curve.
Value within [2000, 6000]
:return: the gain (in nnn)
:type: `int`
"""
)
max_power = unitful_property(
"pmax",
units=pq.W,
format_code="{:.1f}",
set_fmt="{}={}",
valid_range=(0.1*pq.W, 18.0*pq.W),
doc="""
Gets/sets the maximum power
:return: The maximum power
:units: Watts (linear units)
:type: `~quantities.quantity.Quantity`
"""
)