From c0b928f2f623883ce0abb3d8678ad4fe64323f4b Mon Sep 17 00:00:00 2001 From: Oksana Shliakhtun Date: Tue, 7 Nov 2023 13:26:16 +0100 Subject: [PATCH] Driver for ThermoHaake Phoenix P1 Circulator Change-Id: I0573eeac2e40b4715072661c819701186733bf94 --- cfg/phoenix_cfg.py | 16 ++++++ frappy_psi/haake.py | 124 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 cfg/phoenix_cfg.py create mode 100644 frappy_psi/haake.py diff --git a/cfg/phoenix_cfg.py b/cfg/phoenix_cfg.py new file mode 100644 index 0000000..96f6369 --- /dev/null +++ b/cfg/phoenix_cfg.py @@ -0,0 +1,16 @@ +Node('phoenixtest.psi.ch', + 'phoenix test', + 'tcp://5000', + ) + +Mod('io', + 'frappy_psi.haake.HaakeIO', + 'connection for Thermo Haake', + uri='tcp://ldmprep7-ts:3005', + ) + +Mod('T', + 'frappy_psi.haake.TemperatureLoop', + 'holder temperature', + io='io', + ) diff --git a/frappy_psi/haake.py b/frappy_psi/haake.py new file mode 100644 index 0000000..e00b28c --- /dev/null +++ b/frappy_psi/haake.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# 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 +# ***************************************************************************** +import re +import time +from frappy.core import StringIO, HasIO, Parameter, FloatRange, BoolType, \ + EnumType, ERROR, IDLE, WARN, BUSY, Drivable, Command +from frappy_psi.convergence import HasConvergence +from frappy.errors import CommunicationFailedError + +def convert(string): + number = re.sub(r'[^0-9.-]', '', string) + return float(number) + + +class HaakeIO(StringIO): + end_of_line = b'\r' + identification = [('V', r'')] # version of the software + + +class TemperatureLoop(HasIO, HasConvergence, Drivable): + ioClass = HaakeIO + value = Parameter('temperature', unit='degC') + target = Parameter('target', datatype=FloatRange(-50, 200), unit='degC', readonly=False) + setpoint = Parameter('setpoint', datatype=FloatRange, unit='degC') + control_active = Parameter('control on/off', BoolType, readonly=False, default=False) + mode = Parameter('internal/external control', EnumType(int=1, ext=2), readonly=False, default=1) + + status_messages = [ + (ERROR, 'Alarm overtemperature', 3, 1), + (ERROR, 'Alarm liquid level', 4, 1), + (ERROR, 'Alarm motor and pump overloading', 5, 1), + (ERROR, 'Alarm via external connection', 6, 1), + (ERROR, 'Alarm cooling', 7, 1), + (ERROR, 'Alarm Fuzzy control', 8, 1), + (ERROR, 'Alarm internal Pt100', 10, 1), + (ERROR, 'Alarm external Pt100', 11, 1), + (WARN, 'Main relay missing', 2, 1), + (IDLE, 'Temperature control is OFF', 0, 0), + (IDLE, 'Temperature control is on', 0, 1), + ] + + def get_values_status(self): + reply = self.communicate('B') + string = reply.rstrip('$') + return [int(val) for val in string] + + def read_status(self): # control_active update + values_str = self.get_values_status() + self.read_control_active() + + for status_type, status_msg, position, value in self.status_messages: + if values_str[position] == value: # error first + conv_status = HasConvergence.read_status(self) + if self.isBusy(conv_status): + return BUSY, conv_status[1] if 'tolerance' in conv_status[1] else status_msg + return status_type, status_msg # error - needs to reset + return IDLE, '' + + def read_value(self): + if self.mode == 1: + value = self.communicate('F1') + else: + value = self.communicate('F2') + return convert(value) + + def write_control_active(self, value): + if value is True: + self.communicate('GO') # heating and pump run + self.communicate('W SR') # regulation + else: + self.communicate('ST') # heating and pump stop + self.communicate('W ER') + return value + + def read_control_active(self): + values_str = self.get_values_status() + if values_str[0] == 1: + return True + else: + return False + + def read_setpoint(self): + string = self.communicate('S') + return convert(string) + + def write_target(self, target): + self.write_control_active(True) + self.read_status() + self.communicate('W TE C') + self.communicate(f'W SW {target}') + return target + + def write_mode(self, mode): + if mode == 1: + self.communicate('W IN') + self.communicate('W EX') + return mode + + @Command + def clear_errors(self): + """ Reset after error""" + if self.read_status()[0] == ERROR: + try: + self.communicate('ER') + except CommunicationFailedError: + time.sleep(2) # wait for reset to be complete +