161 lines
5.3 KiB
Python
161 lines
5.3 KiB
Python
# *****************************************************************************
|
|
# 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:
|
|
# Anik Stark <anik.stark@psi.ch>
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
# *****************************************************************************
|
|
|
|
"""Bronkhorst flow or pressure regulators. Communication via ProPar protocol.
|
|
|
|
write command: :LnAd01PrTpData\r\n
|
|
read command: :LnAd04CopyPrTp\r\n (Ln = 06)
|
|
answer: :LnAd02CopyData\r\n
|
|
|
|
Ln: number of bytes (hex digits pairs) following
|
|
Ad: node address (3...120, always a reply if message is sent to node address 128)
|
|
Copy: values just to be copied by the reply (first and third digit < 8)
|
|
recommended practice: Use PrTp for Copy
|
|
Pr: Process number (<80, manual page 24pp)
|
|
Tp: Type + parameter number. Type: 00 byte, 20 int, 40 long/float, 60 string
|
|
for strings either 00 (for nul terminated) or the max. number of chars
|
|
has to be appended to the type
|
|
Data: length depending on type.
|
|
|
|
read command for direct communication: :06800401210120
|
|
|
|
Send values on a scale from 0-32000 (0-100%).
|
|
"""
|
|
|
|
from frappy.core import StringIO, HasIO, Readable, Writable, Drivable, Parameter, Property, \
|
|
FloatRange, BoolType, EnumType, IntRange, IDLE, BUSY
|
|
from frappy.errors import CommunicationFailedError
|
|
|
|
|
|
class IO(StringIO):
|
|
end_of_line = '\r\n' # hex: 0D0A
|
|
adr = 128
|
|
identification = [(f':07{adr:02X}047163716300', f':10{adr:02X}02716300.*')] # serial number
|
|
default_settings = {'baudrate': 38400}
|
|
|
|
|
|
def intpar(process, parameter):
|
|
return '06', f'{process:02X}{parameter|0x20:02X}'
|
|
|
|
def longpar(process, parameter):
|
|
return '08', f'{process:02X}{parameter|0x40:04X}'
|
|
|
|
MEASURE = intpar(1, 0)
|
|
SETPOINT = intpar(1, 1)
|
|
RAMP = intpar(1, 2)
|
|
CONTROL = longpar(114, 1)
|
|
|
|
|
|
class Sensor(HasIO, Readable):
|
|
|
|
ioClass = IO
|
|
|
|
value = Parameter('pressure', FloatRange())
|
|
scale = Property('scale factor', FloatRange(), default=1)
|
|
adr = Property('node adress', IntRange(0, 255))
|
|
|
|
def get_par(self, length, param, scale):
|
|
reply = self.communicate(f':{length}{self.adr:02X}04{param}{param}')
|
|
if reply[:11] != f':{length}{self.adr:02X}02{param}':
|
|
return CommunicationFailedError(f'bad reply: {reply}')
|
|
val = int(reply[11:14], 16) / 32000 * scale
|
|
return val
|
|
|
|
def read_value(self):
|
|
return self.get_par(*MEASURE, self.scale)
|
|
|
|
|
|
class Controller(Sensor, Writable):
|
|
|
|
def set_par(self, length, param, scale, value):
|
|
reply = self.communicate(f':{length}{self.adr:02X}01{param}{round(value/scale):04X}')
|
|
if reply[:8] != f':04{self.adr:02X}0000':
|
|
raise CommunicationFailedError(f'bad reply: {reply}')
|
|
return self.get_par(length, param, scale)
|
|
|
|
def read_target(self):
|
|
return self.get_par(*SETPOINT, self.scale)
|
|
|
|
def write_target(self, value):
|
|
val = value / self.scale * 32000
|
|
return self.set_par(*SETPOINT, self.scale, val)
|
|
|
|
|
|
class HasRamp(Drivable):
|
|
|
|
setpoint = Parameter('running setpoint', FloatRange())
|
|
ramp_enable = Parameter('enable ramp mode', BoolType())
|
|
ramp = Parameter('slope of ramp', FloatRange(1e-6, unit='mbar/min'))
|
|
tolerance = Property('tolerance for target vs. running setpoint', FloatRange(), default=1)
|
|
|
|
def read_target(self):
|
|
# overwrite Controller.read_target() as setpoint is running
|
|
return self.read_target
|
|
|
|
def write_target(self, target):
|
|
super().write_target(target)
|
|
self.status = BUSY, 'ramping'
|
|
|
|
def read_setpoint(self):
|
|
return super().read_target()
|
|
|
|
def read_ramp(self):
|
|
if abs(self.read_setpoint() - self.target) < self.tolerance:
|
|
self.status = IDLE, ''
|
|
|
|
def write_ramp(self, ramp):
|
|
if self.ramp_enable:
|
|
time = min(self.scale / ramp, 3000)
|
|
return self.set_par(*RAMP, (60 / 0.1), time)
|
|
|
|
def write_ramp_enable(self, flag):
|
|
if flag:
|
|
self.write_ramp(self.ramp)
|
|
else:
|
|
self.set_par(*RAMP, (60 / 0.1), 0)
|
|
|
|
|
|
class HasControlMode():
|
|
|
|
control_active = Parameter('control mode active', BoolType())
|
|
control = Property('control mode', EnumType(manual=4, loop=11))
|
|
output = Parameter('valve output', FloatRange(), readonly=False)
|
|
|
|
def write_control(self, value):
|
|
if self.control_active:
|
|
val = self.control.get(value, 4)
|
|
return self.set_par(*CONTROL, 1, val)
|
|
|
|
def write_output(self, value):
|
|
scale = (2**24 - 1) / 100
|
|
self.set_par(*CONTROL, scale, value)
|
|
|
|
|
|
class ControllerRamp(HasRamp, Controller):
|
|
pass
|
|
|
|
|
|
class ControllerControlMode(HasControlMode, Controller):
|
|
pass
|
|
|
|
|
|
class ControllerRampControlMode(HasRamp, HasControlMode, Controller):
|
|
pass
|