# ***************************************************************************** # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Module authors: # Oksana Shliakhtun # ***************************************************************************** """Temperature Controller TC1 Quantum NorthWest""" from frappy.core import Readable, Parameter, FloatRange, IDLE, ERROR, BoolType,\ StringIO, HasIO, Property, WARN, Drivable, BUSY, StringType, Done from frappy.errors import InternalError class QnwIO(StringIO): """communication with TC1""" end_of_line = ']' # no line feed! identification = [('[F1 VN ?', r'.F1 VN .*')] # Controller Firmware Version, holder number class SensorTC1(HasIO, Readable): ioClass = QnwIO value = Parameter(unit='degC', min=-15, max=120) channel = Property('channel name', StringType()) def set_param(self, adr, value=None): """ Set parameter. Every command starts with "[F1", and the end of line is "]". :param adr: second part of the command :param value: value to set :return: value converted to float """ short = adr.split()[0] # try 3 times in case we got an asynchronous message for _ in range(3): if value is None: reply = self.communicate(f'[F1 {adr} ?').split() else: reply = self.communicate(f'[F1 {adr} {value}][F1 {short} ?').split() if reply[1] == short: break else: raise InternalError(f'bad reply {reply}') try: return float(reply[2]) except ValueError: return reply[2] def get_param(self, adr): return self.set_param(adr) def read_value(self): return self.get_param(self.channel) def read_status(self): dt = self.parameters['value'].datatype if dt.min <= self.value <= dt.max: return IDLE, '' return ERROR, 'value out of range (cable unplugged?)' class TemperatureLoopTC1(SensorTC1, Drivable): value = Parameter('temperature', unit='degC') target = Parameter('setpoint', unit='degC', min=-5, max=110) control = Parameter('temperature control flag', BoolType(), readonly=False) ramp = Parameter('ramping value', FloatRange, unit='degC/min', readonly=False) ramp_used = Parameter('ramping status', BoolType(), default=False, readonly=False) target_min = Parameter('lowest target temperature', FloatRange, unit='degC') target_max = Parameter('maximum target temperature', FloatRange, unit='degC') def read_target_min(self): return self.get_param('LT') def read_target_max(self): return self.get_param('MT') def read_status(self): """ the device returns 4 symbols according to the current status. These symbols are: ”0” or “1” - number of unreported errors ”+” or “-” - stirrer is on/off ”+” or ”-” - temperature control is on/off ”S” or “C” - current sample holder tempeerature is stable/changing There could be the fifth status symbol: ”+” or “-” or “W” - rampping is on/off/waiting :return: status messages """ status = super().read_status() if status[0] == ERROR: return status reply = self.get_param('IS') # instrument status if len(reply) < 5: self.set_param('IS', 'E+') reply = self.get_param('IS') # instrument status self.control = reply[2] == '+' if reply[4] == '+': return BUSY, 'ramping' if reply[3] == 'C': if self.control: if self.ramp_used: return BUSY, 'stabilizing' return BUSY, 'changing' if self.control: return IDLE, '' return WARN, 'control off' def write_target(self, target): if self.ramp_used: self.set_param('RR S', self.ramp) else: self.set_param('RR S', 0) target = self.set_param('TT S', target) self.set_param('TC', '+') self.read_status() return target def read_target(self): return self.get_param('TT') def write_control(self, control): if control: if not self.read_control(): self.write_target(self.value) return True self.set_param('TC', '-') return False def read_ramp(self): return float(self.get_param('RR')) def write_ramp(self, ramp): ramp = max(0.01, abs(ramp)) self.ramp_used = True ramp = self.set_param('RR S', ramp) if self.control: self.ramp = ramp self.write_target(self.target) return Done return ramp def write_ramp_used(self, value): if self.control: self.ramp_used = value self.write_target(self.target) return Done return value def stop(self): """stop at current value (does nothing if ramp is not used)""" if self.control and self.ramp_used: self.write_target(self.value)