frappy_psi.oiclassic: add IGH (not finished)
This commit is contained in:
@@ -18,12 +18,12 @@
|
|||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
# Anik Stark <anik.stark@psi.ch>
|
# Anik Stark <anik.stark@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""older generation (classic) oxford instruments"""
|
"""oxford instruments old (classic) devices (ILM, IGH, IPS)"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from frappy.core import Parameter, Property, EnumType, FloatRange, IntRange, BoolType, \
|
from frappy.core import Parameter, Property, EnumType, FloatRange, IntRange, BoolType, StringType, \
|
||||||
StringIO, HasIO, Readable, Writable, Drivable, IDLE, WARN, ERROR
|
StringIO, HasIO, Readable, Writable, Drivable, IDLE, BUSY, WARN, ERROR, Attached
|
||||||
from frappy.lib import formatStatusBits
|
from frappy.lib import formatStatusBits
|
||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
from frappy.errors import BadValueError, HardwareError, CommunicationFailedError
|
from frappy.errors import BadValueError, HardwareError, CommunicationFailedError
|
||||||
@@ -32,19 +32,19 @@ from frappy.states import Retry
|
|||||||
|
|
||||||
|
|
||||||
def bit(x, pos):
|
def bit(x, pos):
|
||||||
|
"""Check if the bit at a certain position is set"""
|
||||||
return bool(x & (1 << pos))
|
return bool(x & (1 << pos))
|
||||||
|
|
||||||
|
|
||||||
class Base(HasIO):
|
class OxBase(HasIO):
|
||||||
|
|
||||||
def query(self, cmd, scale=None):
|
def query(self, cmd, scale=None):
|
||||||
reply = self.communicate(cmd)
|
reply = self.communicate(cmd)
|
||||||
if reply[0] != cmd[0]:
|
if reply[0] != cmd[0]:
|
||||||
raise CommunicationFailedError(f'bad reply: {reply} to command {cmd}')
|
raise CommunicationFailedError(f'bad reply: {reply} to command {cmd}')
|
||||||
try:
|
if scale is None:
|
||||||
|
return int(reply[1:])
|
||||||
return float(reply[1:]) * scale
|
return float(reply[1:]) * scale
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def change(self, cmd, value, scale=None):
|
def change(self, cmd, value, scale=None):
|
||||||
try:
|
try:
|
||||||
@@ -86,7 +86,7 @@ limit_map = {'0': (IDLE, ''),
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Field(Base, Magfield):
|
class Field(OxBase, Magfield):
|
||||||
""" read commands:
|
""" read commands:
|
||||||
R1 measured power supply voltage (V)
|
R1 measured power supply voltage (V)
|
||||||
R7 demand field (output field) (T)
|
R7 demand field (output field) (T)
|
||||||
@@ -340,7 +340,7 @@ class ILM_IO(StringIO):
|
|||||||
timeout = 5
|
timeout = 5
|
||||||
|
|
||||||
|
|
||||||
class Level(Base, Readable):
|
class Level(OxBase, Readable):
|
||||||
|
|
||||||
""" X code: XcccSuuvvwwRzz
|
""" X code: XcccSuuvvwwRzz
|
||||||
c: position corresponds to channel 1, 2, 3
|
c: position corresponds to channel 1, 2, 3
|
||||||
@@ -379,6 +379,7 @@ class Level(Base, Readable):
|
|||||||
|
|
||||||
class HeLevel(Level):
|
class HeLevel(Level):
|
||||||
|
|
||||||
|
value = Parameter('He level', FloatRange(unit='%'))
|
||||||
fast = Parameter('switching fast/slow', datatype=BoolType(), readonly=False)
|
fast = Parameter('switching fast/slow', datatype=BoolType(), readonly=False)
|
||||||
CHANNEL = 1
|
CHANNEL = 1
|
||||||
MEDIUM = 'He'
|
MEDIUM = 'He'
|
||||||
@@ -405,110 +406,260 @@ class N2Level(Level):
|
|||||||
return IDLE, ''
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
VALVE_MAP = {'01': 'V9',
|
VALVE_MAP = {'V9': 1,
|
||||||
'02': 'V8',
|
'V8': 2,
|
||||||
'03': 'V7',
|
'V7': 3,
|
||||||
'04': 'V11A',
|
'V11A': 4,
|
||||||
'05': 'V13A',
|
'V13A': 5,
|
||||||
'06': 'V13B',
|
'V13B': 6,
|
||||||
'07': 'V11B',
|
'V11B': 7,
|
||||||
'08': 'V12B',
|
'V12B': 8,
|
||||||
'09': 'He4 rotary pump',
|
'rotary_pump_He4': 9,
|
||||||
'10': 'V1',
|
'V1': 10,
|
||||||
'11': 'V5',
|
'V5': 11,
|
||||||
'12': 'V4',
|
'V4': 12,
|
||||||
'13': 'V3',
|
'V3': 13,
|
||||||
'14': 'V14',
|
'V14' : 14,
|
||||||
'15': 'V10',
|
'V10': 15,
|
||||||
'16': 'V2',
|
'V2': 16,
|
||||||
'17': 'V2A He4',
|
'V2A_He4': 17,
|
||||||
'18': 'V1A He4',
|
'V1A_He4': 18,
|
||||||
'19': 'V5A He4',
|
'V5A_He4': 19,
|
||||||
'20': 'V4A He4',
|
'V4A_He4': 20,
|
||||||
'21': 'V3A He4',
|
'V3A_He4': 21,
|
||||||
'22': 'roots pump',
|
'roots_pump': 22,
|
||||||
'23': 'unlabeled pump',
|
'unlabeled_pump': 23,
|
||||||
'24': 'He3 rotary pump',
|
'rotary_pump_He3': 24,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IGH_IO(StringIO):
|
class IGH_IO(StringIO):
|
||||||
"""oxford instruments dilution gas handling Kelvinox IGH"""
|
""" oxford instruments dilution gas handling Kelvinox IGH
|
||||||
|
|
||||||
|
X code: XxAaCcPpppSsOoEe
|
||||||
|
x motorized valves are still initializing
|
||||||
|
a mix heater activity
|
||||||
|
c control status (0, 1, 2, 3)
|
||||||
|
pppp 4 hex numbers (two digits each), state of solenoid valves and pumps
|
||||||
|
s hex digit, state of the 3 motorized valves
|
||||||
|
o still and sorb heater information
|
||||||
|
e mix heater power range """
|
||||||
|
|
||||||
end_of_line = '\r'
|
end_of_line = '\r'
|
||||||
identification = [('V', r'IGH.*')]
|
identification = [('V', r'IGH.*')]
|
||||||
default_settings = {'baudrate': 9600}
|
default_settings = {'baudrate': 9600}
|
||||||
|
|
||||||
X_PATTERN = re.compile(r'X(\d)A\dC\dP([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})S[0-9A-F]O\dE\d$')
|
X_PATTERN = re.compile(r'X(\d)A\dC\dP([0-9A-F]{8})S([0-9A-F])O(\d)E(\d)$')
|
||||||
statusbits = 0
|
_ini_valves = 0 # ini status of motorized valves
|
||||||
ini_valves = 0
|
_valves = '' # status of solenoid valves and pumps
|
||||||
|
_heater_range = 0
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
reply = self.communicate('X')
|
reply = self.communicate('X')
|
||||||
match = self.X_PATTERN.match(reply)
|
match = self.X_PATTERN.match(reply)
|
||||||
if match:
|
if match:
|
||||||
statuslist = match.groups()
|
ini_valves, valves, motor_status, heater_status, heater_range = match.groups()
|
||||||
# REVISE THE METHOD FROM HERE ON
|
self._ini_valves = int(ini_valves, 16)
|
||||||
# CHECK JUPYTER NOTEBOOK
|
self._valves = int(valves, 16)
|
||||||
# a hex digit indicating if motorized valves are still initializing
|
self._motor_status = int(motor_status, 16)
|
||||||
self.ini_valves = int(statuslist[0], 16)
|
self._heater_status = int(heater_status)
|
||||||
|
self._heater_range = int(heater_range)
|
||||||
|
|
||||||
|
|
||||||
class Valve(Base, Writable):
|
class Valve(OxBase, Writable):
|
||||||
|
|
||||||
ioClass = IGH_IO
|
ioClass = IGH_IO
|
||||||
|
|
||||||
target = Parameter('open or close valve', EnumType(open=1, close=0))
|
value = Parameter('state of valve (open or close)', datatype=IntRange(0,1))
|
||||||
addr = Property('valve number', IntRange(1,24))
|
target = Parameter('open or close valve', datatype=EnumType(open=1, close=0))
|
||||||
|
addr = Property('valve name', datatype=EnumType(VALVE_MAP))
|
||||||
|
|
||||||
def write_target(self, val):
|
def read_value(self):
|
||||||
|
# hex -> int -> check if bit in bin(integer) is set at the addr position
|
||||||
|
return bit(self.io._valves, self.addr.value - 1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
# open: 2N, close: 2N + 1
|
# open: 2N, close: 2N + 1
|
||||||
self.change('P', (2 * self.addr + 1 - int(val)), 1)
|
self.change('P', (2 * self.addr.value + 1 - int(target)), 1)
|
||||||
|
|
||||||
def read_status(self):
|
|
||||||
# CHECK
|
|
||||||
status = self.io.statusbits & (1 << self.addr)
|
|
||||||
if status is not None:
|
|
||||||
return status
|
|
||||||
return IDLE, ''
|
|
||||||
|
|
||||||
|
|
||||||
class PulsedValve(Valve):
|
class PulsedValve(Valve):
|
||||||
|
|
||||||
delay = Parameter('time valve is open', FloatRange(unit='s'))
|
delay = Parameter('delay (time valve is open)', FloatRange(unit='s'))
|
||||||
_start = 0
|
_start = 0
|
||||||
|
|
||||||
def write_target(self, val):
|
def write_target(self, target):
|
||||||
if val:
|
if target:
|
||||||
self._start = time.time()
|
self._start = time.time()
|
||||||
self.setFastPoll(True, 0.01)
|
self.setFastPoll(True, 0.01)
|
||||||
else:
|
else:
|
||||||
self.setFastPoll(False)
|
self.setFastPoll(False)
|
||||||
self.change('P', (2 * self.addr + 1 - int(val)), 1)
|
self.change('P', (2 * self.addr.value + 1 - int(target)), 1)
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
super().doPoll()
|
super().doPoll()
|
||||||
if self._start:
|
if self._start:
|
||||||
if time.time() > self._start + self.delay:
|
if time.time() > self._start + self.delay:
|
||||||
self.write_target(False) # turn valve off
|
self.write_target(0)
|
||||||
#self.setFastPoll(False)
|
|
||||||
self._start = 0
|
self._start = 0
|
||||||
|
|
||||||
|
|
||||||
class MotorValve(Base, Drivable):
|
class MotorValve(OxBase, Writable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
# TODO: class for valve 12 --> based on SlowMotorValve, but arrives instantanous, no busy state
|
||||||
|
|
||||||
|
|
||||||
|
class SlowMotorValve(OxBase, Drivable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('target of motor valve', datatype=FloatRange(0, 100, unit='%'))
|
||||||
|
motor_valve = Property('motor valves', datatype=StringType('V6'))
|
||||||
|
value = Parameter('position of valve', datatype=FloatRange(0, 100, unit='%'))
|
||||||
|
_prev_time = 0
|
||||||
|
_prev = 0
|
||||||
|
_direction = 0
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self._prev_time = time.time()
|
||||||
|
self.change('G', target, 0.1)
|
||||||
|
self._direction = (target > self._prev) - (target < self._prev)
|
||||||
|
self._prev = target
|
||||||
|
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if str(self.io._ini_valves) == '001':
|
||||||
|
return BUSY, 'valve 6 is initializing'
|
||||||
|
# TODO: estimate position of valve (update, and adapt if changing direction inbetween)
|
||||||
|
self.value = self.value + self._direction * (time.time() - self._prev_time) * 100
|
||||||
|
if (self.io._motor_status >> 1) & 1:
|
||||||
|
return BUSY, 'valve is moving'
|
||||||
|
else:
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
GAUGE_MAP = {'G1': 14,
|
||||||
|
'G2': 15,
|
||||||
|
'G3': 16,
|
||||||
|
'P1': 20,
|
||||||
|
'P2': 21,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Pressure(OxBase, Readable):
|
||||||
|
|
||||||
|
gauge_addr = Property('pressure gauge address', datatype=EnumType(GAUGE_MAP))
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
nr = GAUGE_MAP[self.gauge_addr]
|
||||||
|
if self.gauge_addr.startswith('G'):
|
||||||
|
return self.query(f'R{nr}', 0.1)
|
||||||
|
return self.query(f'R{nr}', 1)
|
||||||
|
|
||||||
|
|
||||||
|
class MixPower(OxBase, Writable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('mix power', datatype=FloatRange(0, 0.02, unit='W'))
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
scale = 10**-(7 - self.io._heater_range)
|
||||||
|
return self.query('R4', scale)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if target:
|
||||||
|
self.command('A1') # on, fixed heater power
|
||||||
|
target = min(0.01999, target)
|
||||||
|
target_nW = str(int(target * 1e9))
|
||||||
|
range_mix = max(1, len(target_nW) - 3)
|
||||||
|
scale = 10**-(7 - range_mix)
|
||||||
|
self.command(f'E{range_mix}')
|
||||||
|
self.change('M', target, scale)
|
||||||
|
else:
|
||||||
|
self.command('A0') # turn off
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
#
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Pressure(Base, Readable):
|
class SorbPower(OxBase, Writable):
|
||||||
pass
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('sorb power', datatype=FloatRange(0.001, 2, unit='W'))
|
||||||
|
writecmd = 'B' # in units of 1mW (range 0000 to 1999)
|
||||||
|
scale = 1e-3
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self.io._heater_status & 6:
|
||||||
|
return self.query('R6', self.scale)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if target:
|
||||||
|
self.change('O', self.io._heater_status & 1 | 4)
|
||||||
|
else:
|
||||||
|
self.change('O', self.io._heater_status & 1)
|
||||||
|
self.change('B', target, self.scale)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
sorb_status = self.io._heater_status & 6
|
||||||
|
if sorb_status == 2:
|
||||||
|
return WARN, 'sorb in control mode'
|
||||||
|
return IDLE, ('on' if sorb_status else 'off')
|
||||||
|
|
||||||
|
|
||||||
class Heater(Base, Writable):
|
class StillPower(OxBase, Writable):
|
||||||
pass
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('still power', datatype=FloatRange(0.0001, 0.2, unit='W'))
|
||||||
|
readcmd = 'R5'
|
||||||
|
writecmd = 'S' # in units of 0.1mW (range 0000 to 1999)
|
||||||
|
scale = 1e-4
|
||||||
|
|
||||||
|
# bit-wise arithmetik analog SorbPower (zu checkende position wechseln)
|
||||||
|
|
||||||
|
|
||||||
class N2Sensor():
|
class N2Sensor(Readable):
|
||||||
# ask Markus
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Pump(Base, Writable):
|
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||||
pass
|
|
||||||
|
|
||||||
|
class PumpFeedback(Valve):
|
||||||
|
|
||||||
|
value = Parameter('pump feedback', datatype=BoolType())
|
||||||
|
upper_LN2 = Attached()
|
||||||
|
lower_LN2 = Attached()
|
||||||
|
PATTERN = re.compile(r'?(\d),(\d),(\d)')
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
reply = self.communicate('{r}')
|
||||||
|
match = self.PATTERN.match(reply)
|
||||||
|
if match:
|
||||||
|
self.value, self.upper_LN2, self.lower_LN2 = match.groups()
|
||||||
|
self.upper_LN2 = 0.1 * self.upper_LN2
|
||||||
|
self.lower_LN2 = 0.1 * self.lower_LN2
|
||||||
|
# lesen: {r}
|
||||||
|
# N2 sensor und pumpe laufen hin ueber arduino, kommando {cmd},
|
||||||
|
# zuruck via IGH, welches ? voranstellt,
|
||||||
|
# antwort ist '?{\d,\d,\d}' # 0,1 pumpe on/off , widerstand * 0.1 (lower), widerstand * 0.1 (upper)
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
# hex -> int -> check if bit in bin(integer) is set at the addr position
|
||||||
|
return bit(self.io._valves, self.addr.value - 1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
# open: 2 * 24, close: 2 * 24 + 1
|
||||||
|
self.change('P', 2 * self.addr.value + 1 - target, 1)
|
||||||
|
self.value = target
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if self.target and not self.value:
|
||||||
|
return WARN, 'pump switched off'
|
||||||
|
return IDLE, ''
|
||||||
|
|||||||
Reference in New Issue
Block a user