Compare commits
199 Commits
Author | SHA1 | Date | |
---|---|---|---|
a7fd90cd6d | |||
adfb561308 | |||
70a31b5cae | |||
8ee97ade63 | |||
1715f95dd4 | |||
db29776dd5 | |||
a2905d9fbc | |||
16b826394f | |||
ea8570d422 | |||
1169e0cd09 | |||
7d02498b3d | |||
694b121c01 | |||
0f50de9a7f | |||
b454f47a12 | |||
6e7be6b4c7 | |||
af28511403 | |||
9d9d31693b | |||
3a7fff713d | |||
2acab33faa | |||
8c589cc138 | |||
2b42e3fa0a | |||
5b0da3ba98 | |||
c80b4ac5fb | |||
8cb9154bb5 | |||
813d1b76ef | |||
183709b7ce | |||
2cdf1fc58e | |||
ffaa9c83bd | |||
f9a0fdf7e4 | |||
7dfb2ff4e3 | |||
84c0017c03 | |||
2126956160 | |||
4cdd3b0709 | |||
15d38d7cc1 | |||
9904d31f0b | |||
b07d2ae8a3 | |||
7d7cb02f17 | |||
1017925ca0 | |||
bb14d02884 | |||
4c499cf048 | |||
e403396941 | |||
5b42df4a5e | |||
841ef224f6 | |||
8142ba746d | |||
5358412b7a | |||
010f0747e1 | |||
047c52b5a5 | |||
f846c5cb31 | |||
0e4a427bc3 | |||
2d8b609a3c | |||
6e3865b345 | |||
0004dc7620 | |||
158477792f | |||
fd0e762d18 | |||
a16ec6cc91 | |||
777a2cb6a9 | |||
cb3e98f86d | |||
a8bafde64e | |||
36c512d50b | |||
17b7a01ce1 | |||
be66faa591 | |||
e27b4f72b5 | |||
bc7922f5c8 | |||
99a58933ec | |||
9e000528d2 | |||
4a2ce62dd8 | |||
9e6699dd1e | |||
416cdd5a88 | |||
1bd188e326 | |||
f7b29ee959 | |||
f6a0ccb38b | |||
b93a0cd87b | |||
be6ba73c89 | |||
c075738584 | |||
0fa2e8332d | |||
afb49199a1 | |||
416fe6ddc0 | |||
e3cb5d2e60 | |||
998367a727 | |||
ab918a33ae | |||
397ec2efbd | |||
67032ff59b | |||
03c356590b | |||
06bec41ed3 | |||
4cd6929d4b | |||
a89f7a3c44 | |||
a4330081b7 | |||
3b997d7d86 | |||
612295d360 | |||
9e39a43193 | |||
6adfafaa27 | |||
f6c4090b96 | |||
ecef2b8974 | |||
96a7e2109b | |||
2f3c68a5c5 | |||
e9a195d61e | |||
6ac3938b78 | |||
b4cfdcfc1a | |||
d32fb647a6 | |||
abf7859fd6 | |||
55ea2b8cc4 | |||
27600e3ddf | |||
6b4244f071 | |||
1d81fc6fcd | |||
dfce0bdfbc | |||
c39aef10aa | |||
45dd87060b | |||
8019b359c4 | |||
4c5109e5a3 | |||
bf4b3e5683 | |||
af34fef1e1 | |||
5e1c22ba28 | |||
0bc4a63aa7 | |||
cb2c10655c | |||
6c49abea74 | |||
dee8f8929e | |||
2e143963df | |||
4bc82c2896 | |||
833a68db51 | |||
b9f046a665 | |||
9d9b5b2694 | |||
255adbf8d9 | |||
bc0133f55a | |||
09e59b93d8 | |||
2474dc5e72 | |||
9dab41441f | |||
4af46a0ea2 | |||
b844b83352 | |||
3b63e32395 | |||
5168e0133d | |||
9ea6082ed8 | |||
f205cf76aa | |||
db9ce02028 | |||
c4a39306e4 | |||
024de0bd32 | |||
d2d63c47e1 | |||
565e8e6fd3 | |||
89bc7f6dfe | |||
c69fe1571a | |||
c40033a816 | |||
da37175cbb | |||
2020928289 | |||
9df6794678 | |||
41f3b7526e | |||
f80624b48d | |||
9e2e6074c8 | |||
5a13888498 | |||
5a8a6b88ff | |||
b84b7964e3 | |||
6c5dddc449 | |||
78fa49ef74 | |||
d92b154292 | |||
073fe1a08b | |||
f80c793cd9 | |||
519e9e2ed7 | |||
14036160f7 | |||
131dc60807 | |||
49722a858f | |||
c61b674382 | |||
091543be56 | |||
d2885bdd72 | |||
975593dd6b | |||
4fe28363d3 | |||
28b19dbf57 | |||
05189d094a | |||
47da14eef9 | |||
7904f243cb | |||
a2fed8df03 | |||
19f965bced | |||
3e4ea2515e | |||
714c820115 | |||
a8e1d0e1e8 | |||
d7a1604bd5 | |||
b92095974b | |||
8dc9c57e9d | |||
7c95f1f8ee | |||
3786d2f209 | |||
138b84e84c | |||
997e8e26e9 | |||
644d005dad | |||
36dfe968e8 | |||
0932228596 | |||
dff0c819de | |||
fd917724d8 | |||
bf43858031 | |||
f354b19cf0 | |||
f304ac019e | |||
9a9a22588f | |||
3e26dd49d0 | |||
5a456a82b0 | |||
f6868da3b9 | |||
ee31f8fb45 | |||
a6a3f80e30 | |||
ad36ab1067 | |||
f2d795cfba | |||
c04337c3a4 | |||
57d5298c92 | |||
9a6421a54f | |||
c5d429346d |
22
calibtest.py
Normal file
22
calibtest.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from glob import glob
|
||||||
|
from frappy_psi.calcurve import CalCurve
|
||||||
|
|
||||||
|
os.chdir('/Users/zolliker/gitpsi/calcurves')
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
calib = sys.argv[1]
|
||||||
|
c = CalCurve(calib)
|
||||||
|
else:
|
||||||
|
for file in sorted(glob('*.*')):
|
||||||
|
if file.endswith('.md') or file.endswith('.std'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
c = CalCurve(file)
|
||||||
|
xy = c.export()
|
||||||
|
print('%9.4g %12.7g %9.4g %9.4g %s' % (tuple(c.extx) + tuple(c.exty) + (file,)))
|
||||||
|
except Exception as e:
|
||||||
|
print(file, e)
|
||||||
|
calib = file
|
||||||
|
|
@ -6,7 +6,7 @@ Node('QnwTC1test.psi.ch',
|
|||||||
Mod('io',
|
Mod('io',
|
||||||
'frappy_psi.qnw.QnwIO',
|
'frappy_psi.qnw.QnwIO',
|
||||||
'connection for Quantum northwest',
|
'connection for Quantum northwest',
|
||||||
uri='tcp://ldm-fi-ts:3001',
|
uri='tcp://ldmcc01-ts:3004',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T',
|
Mod('T',
|
||||||
|
@ -6,7 +6,7 @@ Node('TFA10.psi.ch',
|
|||||||
Mod('io',
|
Mod('io',
|
||||||
'frappy_psi.thermofisher.ThermFishIO',
|
'frappy_psi.thermofisher.ThermFishIO',
|
||||||
'connection for ThermoFisher A10',
|
'connection for ThermoFisher A10',
|
||||||
uri='tcp://ldm-fi-ts:3002',
|
uri='tcp://ldmse-d910-ts:3001',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T',
|
Mod('T',
|
||||||
|
@ -24,6 +24,7 @@ Mod('ts_low',
|
|||||||
minrange=13,
|
minrange=13,
|
||||||
range=22,
|
range=22,
|
||||||
tolerance = 0.1,
|
tolerance = 0.1,
|
||||||
|
vexc = 3,
|
||||||
htrrng=4,
|
htrrng=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ Mod('ts_high',
|
|||||||
'sample Cernox',
|
'sample Cernox',
|
||||||
channel = 1,
|
channel = 1,
|
||||||
switcher = 'lsc_channel',
|
switcher = 'lsc_channel',
|
||||||
minrange=9,
|
minrange=11,
|
||||||
|
vexc = 5,
|
||||||
range=22,
|
range=22,
|
||||||
tolerance = 0.1,
|
tolerance = 0.1,
|
||||||
htrrng=5,
|
htrrng=5,
|
||||||
@ -45,6 +47,8 @@ Mod('ts',
|
|||||||
value=Param(unit='K'),
|
value=Param(unit='K'),
|
||||||
low='ts_low',
|
low='ts_low',
|
||||||
high='ts_high',
|
high='ts_high',
|
||||||
|
#min_high=0.6035,
|
||||||
|
#max_low=1.6965,
|
||||||
min_high=0.6,
|
min_high=0.6,
|
||||||
max_low=1.7,
|
max_low=1.7,
|
||||||
tolerance=0.1,
|
tolerance=0.1,
|
||||||
|
19
cfg/attocube_cfg.py
Normal file
19
cfg/attocube_cfg.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Node('attocube_test.psi.ch',
|
||||||
|
'a single attocube axis',
|
||||||
|
interface='tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('r',
|
||||||
|
'frappy_psi.attocube.Axis',
|
||||||
|
'ANRv220-F3-02882',
|
||||||
|
axis = 1,
|
||||||
|
value = Param(unit='deg'),
|
||||||
|
tolerance = 0.1,
|
||||||
|
target_min = 0,
|
||||||
|
target_max = 360,
|
||||||
|
steps_fwd = 45,
|
||||||
|
steps_bwd = 85,
|
||||||
|
step_mode = True,
|
||||||
|
# gear = 1.2,
|
||||||
|
)
|
||||||
|
|
60
cfg/flowsas_cfg.py
Normal file
60
cfg/flowsas_cfg.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
Node('flowsas.psi.ch',
|
||||||
|
'flowsas test motors',
|
||||||
|
'tcp://3000',
|
||||||
|
)
|
||||||
|
|
||||||
|
#Mod('mot_io',
|
||||||
|
# 'frappy_psi.phytron.PhytronIO',
|
||||||
|
# 'io for motor control',
|
||||||
|
# uri = 'serial:///dev/ttyUSB0',
|
||||||
|
# )
|
||||||
|
|
||||||
|
#Mod('hmot',
|
||||||
|
# 'frappy_psi.phytron.Motor',
|
||||||
|
# 'horizontal axis',
|
||||||
|
# axis = 'X',
|
||||||
|
# io = 'mot_io',
|
||||||
|
# encoder_mode = 'NO',
|
||||||
|
# )
|
||||||
|
|
||||||
|
#Mod('vmot',
|
||||||
|
# 'frappy_psi.phytron.Motor',
|
||||||
|
# 'vertical axis',
|
||||||
|
# axis = 'Y',
|
||||||
|
# io = 'mot_io',
|
||||||
|
# encoder_mode= 'NO',
|
||||||
|
# )
|
||||||
|
|
||||||
|
Mod('syr_io',
|
||||||
|
'frappy_psi.cetoni_pump.LabCannBus',
|
||||||
|
'Module for bus',
|
||||||
|
deviceconfig = "/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/conti_flow",
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('syr1',
|
||||||
|
'frappy_psi.cetoni_pump.SyringePump',
|
||||||
|
'First syringe pump',
|
||||||
|
io='syr_io',
|
||||||
|
pump_name = "Nemesys_S_1_Pump",
|
||||||
|
valve_name = "Nemesys_S_1_Valve",
|
||||||
|
inner_diameter_set = 14.5673,
|
||||||
|
piston_stroke_set = 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('syr2',
|
||||||
|
'frappy_psi.cetoni_pump.SyringePump',
|
||||||
|
'Second syringe pump',
|
||||||
|
io='syr_io',
|
||||||
|
pump_name = "Nemesys_S_2_Pump",
|
||||||
|
valve_name = "Nemesys_S_2_Valve",
|
||||||
|
inner_diameter_set = 14.5673,
|
||||||
|
piston_stroke_set = 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('contiflow',
|
||||||
|
'frappy_psi.cetoni_pump.ContiFlowPump',
|
||||||
|
'Continuous flow pump',
|
||||||
|
io='syr_io',
|
||||||
|
inner_diameter_set = 14.5673,
|
||||||
|
piston_stroke_set = 60,
|
||||||
|
)
|
@ -1,16 +0,0 @@
|
|||||||
Node('lockin830test.psi.ch',
|
|
||||||
'lockin830 test',
|
|
||||||
'tcp://5000',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('io',
|
|
||||||
'frappy_psi.SR830.SR830_IO',
|
|
||||||
'lockin communication',
|
|
||||||
uri='tcp://linse-976d-ts:3002',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('XY',
|
|
||||||
'frappy_psi.SR830.XY',
|
|
||||||
'XY channels',
|
|
||||||
io='io',
|
|
||||||
)
|
|
@ -1,34 +0,0 @@
|
|||||||
Node('multimetertest.psi.ch',
|
|
||||||
'multimeter test',
|
|
||||||
'tcp://5000',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('io',
|
|
||||||
'frappy_psi.HP.HP_IO',
|
|
||||||
'multimeter communication',
|
|
||||||
uri='/dev/cu.usbserial-21410',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('Voltage',
|
|
||||||
'frappy_psi.HP.Voltage',
|
|
||||||
'voltage',
|
|
||||||
io='io',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('Current',
|
|
||||||
'frappy_psi.HP.Current',
|
|
||||||
'current',
|
|
||||||
io='io',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('Resistance',
|
|
||||||
'frappy_psi.HP.Resistance',
|
|
||||||
'resistivity',
|
|
||||||
io='io',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('Frequency',
|
|
||||||
'frappy_psi.HP.Frequency',
|
|
||||||
'resistivity',
|
|
||||||
io='io',
|
|
||||||
)
|
|
12
cfg/peristaltic_pump_cfg.py
Normal file
12
cfg/peristaltic_pump_cfg.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Node('flowsas.psi.ch',
|
||||||
|
'peristaltic pump',
|
||||||
|
'tcp://3000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('peripump',
|
||||||
|
'frappy_psi.gilsonpump.PeristalticPump',
|
||||||
|
'Peristaltic pump',
|
||||||
|
addr_AO = 'ao1',
|
||||||
|
addr_dir_relay = 'o1',
|
||||||
|
addr_run_relay = 'o2',
|
||||||
|
)
|
13
cfg/pressureTest_cfg.py
Normal file
13
cfg/pressureTest_cfg.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Node('vf.psi.ch',
|
||||||
|
'small vacuum furnace',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('p',
|
||||||
|
'frappy_psi.ionopimax.VoltageInput',
|
||||||
|
'Vacuum pressure',
|
||||||
|
addr = 'av2',
|
||||||
|
rawrange = (0, 10),
|
||||||
|
valuerange = (0, 10),
|
||||||
|
value = Param(unit='V'),
|
||||||
|
)
|
11
cfg/rheotrigger_cfg.py
Normal file
11
cfg/rheotrigger_cfg.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Node('flowsas.psi.ch',
|
||||||
|
'rheometer triggering',
|
||||||
|
'tcp://3000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('rheo',
|
||||||
|
'frappy_psi.rheo_trigger.RheoTrigger',
|
||||||
|
'Trigger for the rheometer',
|
||||||
|
addr='dt1',
|
||||||
|
doBeep = False,
|
||||||
|
)
|
@ -1,67 +0,0 @@
|
|||||||
Node('bridge.psi.ch',
|
|
||||||
'ac resistance bridge',
|
|
||||||
'tcp://5000',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('io',
|
|
||||||
'frappy_psi.bridge.BridgeIO',
|
|
||||||
'communication to sim900',
|
|
||||||
uri='serial:///dev/cu.usbserial-14340',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('res1',
|
|
||||||
'frappy_psi.bridge.Resistance',
|
|
||||||
'module communication',
|
|
||||||
io='io',
|
|
||||||
port=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('res2',
|
|
||||||
'frappy_psi.bridge.Resistance',
|
|
||||||
'module communication',
|
|
||||||
io='io',
|
|
||||||
port=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('res3',
|
|
||||||
'frappy_psi.bridge.Resistance',
|
|
||||||
'module communication',
|
|
||||||
io='io',
|
|
||||||
port=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('phase1',
|
|
||||||
'frappy_psi.bridge.Phase',
|
|
||||||
'module communication',
|
|
||||||
resistance='res1',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('phase2',
|
|
||||||
'frappy_psi.bridge.Phase',
|
|
||||||
'module communication',
|
|
||||||
resistance='res2',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('phase3',
|
|
||||||
'frappy_psi.bridge.Phase',
|
|
||||||
'module communication',
|
|
||||||
resistance='res3',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('dev1',
|
|
||||||
'frappy_psi.bridge.Deviation',
|
|
||||||
'module communication',
|
|
||||||
resistance='res1',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('dev2',
|
|
||||||
'frappy_psi.bridge.Deviation',
|
|
||||||
'module communication',
|
|
||||||
resistance='res1',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('dev3',
|
|
||||||
'frappy_psi.bridge.Deviation',
|
|
||||||
'module communication',
|
|
||||||
resistance='res3',
|
|
||||||
)
|
|
@ -138,13 +138,6 @@ Mod('T_one_K',
|
|||||||
io='itc',
|
io='itc',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_one_K',
|
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
|
||||||
'1 K plate warmup heater',
|
|
||||||
slot='DB3.H1',
|
|
||||||
io='itc',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('T_mix_wup',
|
Mod('T_mix_wup',
|
||||||
'frappy_psi.mercury.TemperatureLoop',
|
'frappy_psi.mercury.TemperatureLoop',
|
||||||
'mix. chamber warmup temperature',
|
'mix. chamber warmup temperature',
|
||||||
|
@ -232,7 +232,7 @@ class ReadFailedError(SECoPError):
|
|||||||
|
|
||||||
|
|
||||||
class OutOfRangeError(SECoPError):
|
class OutOfRangeError(SECoPError):
|
||||||
"""The requested parameter can not be read just now"""
|
"""The value read from the hardware is out of sensor or calibration range"""
|
||||||
name = 'OutOfRange'
|
name = 'OutOfRange'
|
||||||
|
|
||||||
|
|
||||||
|
304
frappy/extparams.py
Normal file
304
frappy/extparams.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""extended parameters
|
||||||
|
|
||||||
|
special parameter classes with some automatic functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from frappy.core import Parameter, Property
|
||||||
|
from frappy.datatypes import BoolType, DataType, DataTypeType, EnumType, \
|
||||||
|
FloatRange, StringType, StructOf, ValueType
|
||||||
|
from frappy.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
class StructParam(Parameter):
|
||||||
|
"""convenience class to create a struct Parameter together with individual params
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
class Controller(Drivable):
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
ctrlpars = StructParam('ctrlpars struct', [
|
||||||
|
('pid_p', 'p', Parameter('control parameter p', FloatRange())),
|
||||||
|
('pid_i', 'i', Parameter('control parameter i', FloatRange())),
|
||||||
|
('pid_d', 'd', Parameter('control parameter d', FloatRange())),
|
||||||
|
], readonly=False)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
then implement either read_ctrlpars and write_ctrlpars or
|
||||||
|
read_pid_p, read_pid_i, read_pid_d, write_pid_p, write_pid_i and write_pid_d
|
||||||
|
|
||||||
|
the methods not implemented will be created automatically
|
||||||
|
"""
|
||||||
|
|
||||||
|
# use properties, as simple attributes are not considered on copy()
|
||||||
|
paramdict = Property('dict <parametername> of Parameter(...)', ValueType())
|
||||||
|
hasStructRW = Property('has a read_<struct param> or write_<struct param> method',
|
||||||
|
BoolType(), default=False)
|
||||||
|
|
||||||
|
insideRW = 0 # counter for avoiding multiple superfluous updates
|
||||||
|
|
||||||
|
def __init__(self, description=None, paramdict=None, prefix_or_map='', *, datatype=None, readonly=False, **kwds):
|
||||||
|
"""create a struct parameter together with individual parameters
|
||||||
|
|
||||||
|
in addition to normal Parameter arguments:
|
||||||
|
|
||||||
|
:param paramdict: dict <member name> of Parameter(...)
|
||||||
|
:param prefix_or_map: either a prefix for the parameter name to add to the member name
|
||||||
|
or a dict <member name> or <parameter name>
|
||||||
|
"""
|
||||||
|
if isinstance(paramdict, DataType):
|
||||||
|
raise ProgrammingError('second argument must be a dict of Param')
|
||||||
|
if datatype is None and paramdict is not None: # omit the following on Parameter.copy()
|
||||||
|
if isinstance(prefix_or_map, str):
|
||||||
|
prefix_or_map = {m: prefix_or_map + m for m in paramdict}
|
||||||
|
for membername, param in paramdict.items():
|
||||||
|
param.name = prefix_or_map[membername]
|
||||||
|
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
|
||||||
|
kwds['influences'] = [p.name for p in paramdict.values()]
|
||||||
|
self.updateEnable = {}
|
||||||
|
if paramdict:
|
||||||
|
kwds['paramdict'] = paramdict
|
||||||
|
super().__init__(description, datatype, readonly=readonly, **kwds)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
# names of access methods of structed param (e.g. ctrlpars)
|
||||||
|
struct_read_name = f'read_{name}' # e.g. 'read_ctrlpars'
|
||||||
|
struct_write_name = f'write_{name}' # e.h. 'write_ctrlpars'
|
||||||
|
self.hasStructRW = hasattr(owner, struct_read_name) or hasattr(owner, struct_write_name)
|
||||||
|
|
||||||
|
for membername, param in self.paramdict.items():
|
||||||
|
pname = param.name
|
||||||
|
changes = {
|
||||||
|
'readonly': self.readonly,
|
||||||
|
'influences': set(param.influences) | {name},
|
||||||
|
}
|
||||||
|
param.ownProperties.update(changes)
|
||||||
|
param.init(changes)
|
||||||
|
setattr(owner, pname, param)
|
||||||
|
param.__set_name__(owner, param.name)
|
||||||
|
|
||||||
|
if self.hasStructRW:
|
||||||
|
rname = f'read_{pname}'
|
||||||
|
|
||||||
|
if not hasattr(owner, rname):
|
||||||
|
def rfunc(self, membername=membername, struct_read_name=struct_read_name):
|
||||||
|
return getattr(self, struct_read_name)()[membername]
|
||||||
|
|
||||||
|
rfunc.poll = False # read_<struct param> is polled only
|
||||||
|
setattr(owner, rname, rfunc)
|
||||||
|
|
||||||
|
if not self.readonly:
|
||||||
|
wname = f'write_{pname}'
|
||||||
|
if not hasattr(owner, wname):
|
||||||
|
def wfunc(self, value, membername=membername,
|
||||||
|
name=name, rname=rname, struct_write_name=struct_write_name):
|
||||||
|
valuedict = dict(getattr(self, name))
|
||||||
|
valuedict[membername] = value
|
||||||
|
getattr(self, struct_write_name)(valuedict)
|
||||||
|
return getattr(self, rname)()
|
||||||
|
|
||||||
|
setattr(owner, wname, wfunc)
|
||||||
|
|
||||||
|
if not self.hasStructRW:
|
||||||
|
if not hasattr(owner, struct_read_name):
|
||||||
|
def struct_read_func(self, name=name, flist=tuple(
|
||||||
|
(m, f'read_{p.name}') for m, p in self.paramdict.items())):
|
||||||
|
pobj = self.parameters[name]
|
||||||
|
# disable updates generated from the callbacks of individual params
|
||||||
|
pobj.insideRW += 1 # guarded by self.accessLock
|
||||||
|
try:
|
||||||
|
return {m: getattr(self, f)() for m, f in flist}
|
||||||
|
finally:
|
||||||
|
pobj.insideRW -= 1
|
||||||
|
|
||||||
|
setattr(owner, struct_read_name, struct_read_func)
|
||||||
|
|
||||||
|
if not (self.readonly or hasattr(owner, struct_write_name)):
|
||||||
|
|
||||||
|
def struct_write_func(self, value, name=name, funclist=tuple(
|
||||||
|
(m, f'write_{p.name}') for m, p in self.paramdict.items())):
|
||||||
|
pobj = self.parameters[name]
|
||||||
|
pobj.insideRW += 1 # guarded by self.accessLock
|
||||||
|
try:
|
||||||
|
return {m: getattr(self, f)(value[m]) for m, f in funclist}
|
||||||
|
finally:
|
||||||
|
pobj.insideRW -= 1
|
||||||
|
|
||||||
|
setattr(owner, struct_write_name, struct_write_func)
|
||||||
|
|
||||||
|
super().__set_name__(owner, name)
|
||||||
|
|
||||||
|
def finish(self, modobj=None):
|
||||||
|
"""register callbacks for consistency"""
|
||||||
|
super().finish(modobj)
|
||||||
|
if modobj:
|
||||||
|
|
||||||
|
if self.hasStructRW:
|
||||||
|
def cb(value, modobj=modobj, structparam=self):
|
||||||
|
for membername, param in structparam.paramdict.items():
|
||||||
|
setattr(modobj, param.name, value[membername])
|
||||||
|
|
||||||
|
modobj.addCallback(self.name, cb)
|
||||||
|
else:
|
||||||
|
for membername, param in self.paramdict.items():
|
||||||
|
def cb(value, modobj=modobj, structparam=self, membername=membername):
|
||||||
|
if not structparam.insideRW:
|
||||||
|
prev = dict(getattr(modobj, structparam.name))
|
||||||
|
prev[membername] = value
|
||||||
|
setattr(modobj, structparam.name, prev)
|
||||||
|
|
||||||
|
modobj.addCallback(param.name, cb)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatEnumParam(Parameter):
|
||||||
|
"""combine enum and float parameter
|
||||||
|
|
||||||
|
Example Usage:
|
||||||
|
|
||||||
|
vrange = FloatEnumParam('sensor range', ['500uV', '20mV', '1V'], 'V')
|
||||||
|
|
||||||
|
The following will be created automatically:
|
||||||
|
|
||||||
|
- the parameter vrange will get a datatype FloatRange(5e-4, 1, unit='V')
|
||||||
|
- an additional parameter `vrange_idx` will be created with an enum type
|
||||||
|
{'500uV': 0, '20mV': 1, '1V': 2}
|
||||||
|
- the method `write_vrange` will be created automatically
|
||||||
|
|
||||||
|
However, the methods `write_vrange_idx` and `read_vrange_idx`, if needed,
|
||||||
|
have to implemented by the programmer.
|
||||||
|
|
||||||
|
Writing to the float parameter involves 'rounding' to the closest allowed value.
|
||||||
|
|
||||||
|
Customization:
|
||||||
|
|
||||||
|
The individual labels might be customized by defining them as a tuple
|
||||||
|
(<index>, <label>, <float value>) where either the index or the float value
|
||||||
|
may be omitted.
|
||||||
|
|
||||||
|
When the index is omitted, the element will be the previous index + 1 or
|
||||||
|
0 when it is the first element.
|
||||||
|
|
||||||
|
Omitted values will be determined from the label, assuming that they use
|
||||||
|
one of the predefined unit prefixes together with the given unit.
|
||||||
|
|
||||||
|
The name of the index parameter is by default '<name>_idx' but might be
|
||||||
|
changed with the idx_name argument.
|
||||||
|
"""
|
||||||
|
# use properties, as simple attributes are not considered on copy()
|
||||||
|
idx_name = Property('name of attached index parameter', StringType(), default='')
|
||||||
|
valuedict = Property('dict <index> of <value>', ValueType(dict))
|
||||||
|
enumtype = Property('dict <label> of <index', DataTypeType())
|
||||||
|
|
||||||
|
# TODO: factor out unit handling, at the latest when needed elsewhere
|
||||||
|
PREFIXES = {'q': -30, 'r': -27, 'y': -24, 'z': -21, 'a': -18, 'f': -15,
|
||||||
|
'p': -12, 'n': -9, 'u': -6, 'µ': -6, 'm': -3,
|
||||||
|
'': 0, 'k': 3, 'M': 6, 'G': 9, 'T': 12,
|
||||||
|
'P': 15, 'E': 18, 'Z': 21, 'Y': 24, 'R': 25, 'Q': 30}
|
||||||
|
|
||||||
|
def __init__(self, description=None, labels=None, unit='',
|
||||||
|
*, datatype=None, readonly=False, **kwds):
|
||||||
|
if labels is None:
|
||||||
|
# called on Parameter.copy()
|
||||||
|
super().__init__(description, datatype, readonly=readonly, **kwds)
|
||||||
|
return
|
||||||
|
if isinstance(labels, DataType):
|
||||||
|
raise ProgrammingError('second argument must be a list of labels, not a datatype')
|
||||||
|
nextidx = 0
|
||||||
|
try:
|
||||||
|
edict = {}
|
||||||
|
vdict = {}
|
||||||
|
for elem in labels:
|
||||||
|
if isinstance(elem, str):
|
||||||
|
idx, label = [nextidx, elem]
|
||||||
|
else:
|
||||||
|
if isinstance(elem[0], str):
|
||||||
|
elem = [nextidx] + list(elem)
|
||||||
|
idx, label, *tail = elem
|
||||||
|
if tail:
|
||||||
|
vdict[idx], = tail
|
||||||
|
edict[label] = idx
|
||||||
|
nextidx = idx + 1
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
raise ProgrammingError('labels must be a list of labels or tuples '
|
||||||
|
'([index], label, [value])') from e
|
||||||
|
pat = re.compile(rf'([+-]?\d*\.?\d*) *({"|".join(self.PREFIXES)}){unit}$')
|
||||||
|
try:
|
||||||
|
# determine missing values from labels
|
||||||
|
for label, idx in edict.items():
|
||||||
|
if idx not in vdict:
|
||||||
|
value, prefix = pat.match(label).groups()
|
||||||
|
vdict[idx] = float(f'{value}e{self.PREFIXES[prefix]}')
|
||||||
|
except (AttributeError, ValueError) as e:
|
||||||
|
raise ProgrammingError(f"{label!r} has not the form '<float><prefix>{unit}'") from e
|
||||||
|
try:
|
||||||
|
enumtype = EnumType(**edict)
|
||||||
|
except TypeError as e:
|
||||||
|
raise ProgrammingError(str(e)) from e
|
||||||
|
datatype = FloatRange(min(vdict.values()), max(vdict.values()), unit=unit)
|
||||||
|
super().__init__(description, datatype, enumtype=enumtype, valuedict=vdict,
|
||||||
|
readonly=readonly, **kwds)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
super().__set_name__(owner, name)
|
||||||
|
if not self.idx_name:
|
||||||
|
self.idx_name = name + '_idx'
|
||||||
|
iname = self.idx_name
|
||||||
|
idx_param = Parameter(f'index of {name}', self.enumtype,
|
||||||
|
readonly=self.readonly, influences={name})
|
||||||
|
idx_param.init({})
|
||||||
|
setattr(owner, iname, idx_param)
|
||||||
|
idx_param.__set_name__(owner, iname)
|
||||||
|
|
||||||
|
self.setProperty('influences', {iname})
|
||||||
|
|
||||||
|
if not hasattr(owner, f'write_{name}'):
|
||||||
|
|
||||||
|
# customization (like rounding up or down) might be
|
||||||
|
# achieved by adding write_<name>. if not, the default
|
||||||
|
# is rounding to the closest value
|
||||||
|
|
||||||
|
def wfunc(mobj, value, vdict=self.valuedict, fname=name, wfunc_iname=f'write_{iname}'):
|
||||||
|
getattr(mobj, wfunc_iname)(
|
||||||
|
min(vdict, key=lambda i: abs(vdict[i] - value)))
|
||||||
|
return getattr(mobj, fname)
|
||||||
|
|
||||||
|
setattr(owner, f'write_{name}', wfunc)
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
"""getter for value"""
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
return self.valuedict[instance.parameters[self.idx_name].value]
|
||||||
|
|
||||||
|
def trigger_setter(self, modobj, _):
|
||||||
|
# trigger update of float parameter on change of enum parameter
|
||||||
|
modobj.announceUpdate(self.name, getattr(modobj, self.name))
|
||||||
|
|
||||||
|
def finish(self, modobj=None):
|
||||||
|
"""register callbacks for consistency"""
|
||||||
|
super().finish(modobj)
|
||||||
|
if modobj:
|
||||||
|
modobj.addCallback(self.idx_name, self.trigger_setter, modobj)
|
@ -61,7 +61,6 @@ class HasIO(Module):
|
|||||||
ioname = opts.get('io') or f'{name}_io'
|
ioname = opts.get('io') or f'{name}_io'
|
||||||
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
|
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
|
||||||
io.callingModule = []
|
io.callingModule = []
|
||||||
srv.modules[ioname] = io
|
|
||||||
srv.secnode.add_module(io, ioname)
|
srv.secnode.add_module(io, ioname)
|
||||||
self.ioDict[self.uri] = ioname
|
self.ioDict[self.uri] = ioname
|
||||||
self.io = ioname
|
self.io = ioname
|
||||||
|
@ -141,6 +141,7 @@ class SequencerMixin:
|
|||||||
return self.Status.IDLE, ''
|
return self.Status.IDLE, ''
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop sequence"""
|
||||||
if self.seq_is_alive():
|
if self.seq_is_alive():
|
||||||
self._seq_stopflag = True
|
self._seq_stopflag = True
|
||||||
|
|
||||||
|
@ -83,15 +83,14 @@ class HasAccessibles(HasProperties):
|
|||||||
override_values.pop(key, None)
|
override_values.pop(key, None)
|
||||||
elif key in accessibles:
|
elif key in accessibles:
|
||||||
override_values[key] = value
|
override_values[key] = value
|
||||||
|
# remark: merged_properties contain already the properties of accessibles of cls
|
||||||
for aname, aobj in list(accessibles.items()):
|
for aname, aobj in list(accessibles.items()):
|
||||||
if aname in override_values:
|
if aname in override_values:
|
||||||
aobj = aobj.copy()
|
|
||||||
value = override_values[aname]
|
value = override_values[aname]
|
||||||
if value is None:
|
if value is None:
|
||||||
accessibles.pop(aname)
|
accessibles.pop(aname)
|
||||||
continue
|
continue
|
||||||
aobj.merge(merged_properties[aname])
|
aobj = aobj.create_from_value(merged_properties[aname], value)
|
||||||
aobj.override(value)
|
|
||||||
# replace the bare value by the created accessible
|
# replace the bare value by the created accessible
|
||||||
setattr(cls, aname, aobj)
|
setattr(cls, aname, aobj)
|
||||||
else:
|
else:
|
||||||
@ -334,8 +333,7 @@ class Module(HasAccessibles):
|
|||||||
self.secNode = srv.secnode
|
self.secNode = srv.secnode
|
||||||
self.log = logger
|
self.log = logger
|
||||||
self.name = name
|
self.name = name
|
||||||
self.valueCallbacks = {}
|
self.paramCallbacks = {}
|
||||||
self.errorCallbacks = {}
|
|
||||||
self.earlyInitDone = False
|
self.earlyInitDone = False
|
||||||
self.initModuleDone = False
|
self.initModuleDone = False
|
||||||
self.startModuleDone = False
|
self.startModuleDone = False
|
||||||
@ -469,8 +467,7 @@ class Module(HasAccessibles):
|
|||||||
apply default when no value is given (in cfg or as Parameter argument)
|
apply default when no value is given (in cfg or as Parameter argument)
|
||||||
or complain, when cfg is needed
|
or complain, when cfg is needed
|
||||||
"""
|
"""
|
||||||
self.valueCallbacks[pname] = []
|
self.paramCallbacks[pname] = []
|
||||||
self.errorCallbacks[pname] = []
|
|
||||||
if isinstance(pobj, Limit):
|
if isinstance(pobj, Limit):
|
||||||
basepname = pname.rpartition('_')[0]
|
basepname = pname.rpartition('_')[0]
|
||||||
baseparam = self.parameters.get(basepname)
|
baseparam = self.parameters.get(basepname)
|
||||||
@ -535,68 +532,46 @@ class Module(HasAccessibles):
|
|||||||
err.report_error = False
|
err.report_error = False
|
||||||
return # no updates for repeated errors
|
return # no updates for repeated errors
|
||||||
err = secop_error(err)
|
err = secop_error(err)
|
||||||
elif not changed and timestamp < (pobj.timestamp or 0) + pobj.omit_unchanged_within:
|
value_err = value, err
|
||||||
|
else:
|
||||||
|
if not changed and timestamp < (pobj.timestamp or 0) + pobj.omit_unchanged_within:
|
||||||
# no change within short time -> omit
|
# no change within short time -> omit
|
||||||
return
|
return
|
||||||
|
value_err = (value,)
|
||||||
pobj.timestamp = timestamp or time.time()
|
pobj.timestamp = timestamp or time.time()
|
||||||
if err:
|
pobj.readerror = err
|
||||||
callbacks = self.errorCallbacks
|
for cbfunc, cbargs in self.paramCallbacks[pname]:
|
||||||
pobj.readerror = arg = err
|
try:
|
||||||
else:
|
cbfunc(*cbargs, *value_err)
|
||||||
callbacks = self.valueCallbacks
|
except Exception:
|
||||||
arg = value
|
pass
|
||||||
pobj.readerror = None
|
|
||||||
if pobj.export:
|
if pobj.export:
|
||||||
self.updateCallback(self, pobj)
|
self.updateCallback(self, pobj)
|
||||||
cblist = callbacks[pname]
|
|
||||||
for cb in cblist:
|
def addCallback(self, pname, callback_function, *args):
|
||||||
try:
|
self.paramCallbacks[pname].append((callback_function, args))
|
||||||
cb(arg)
|
|
||||||
except Exception:
|
|
||||||
# print(formatExtendedTraceback())
|
|
||||||
pass
|
|
||||||
|
|
||||||
def registerCallbacks(self, modobj, autoupdate=()):
|
def registerCallbacks(self, modobj, autoupdate=()):
|
||||||
"""register callbacks to another module <modobj>
|
"""register callbacks to another module <modobj>
|
||||||
|
|
||||||
- whenever a self.<param> changes:
|
whenever a self.<param> changes or changes its error state:
|
||||||
<modobj>.update_<param> is called with the new value as argument.
|
<modobj>.update_param(<value> [, <exc>]) is called,
|
||||||
If this method raises an exception, <modobj>.<param> gets into an error state.
|
where <value> is the new value and <exc> is given only in case of error.
|
||||||
If the method does not exist and <param> is in autoupdate,
|
if the method does not exist, and <param> is in autoupdate
|
||||||
<modobj>.<param> is updated to self.<param>
|
<modobj>.announceUpdate(<pname>, <value>, <exc>) is called
|
||||||
- whenever <self>.<param> gets into an error state:
|
with <exc> being None in case of no error.
|
||||||
<modobj>.error_update_<param> is called with the exception as argument.
|
|
||||||
If this method raises an error, <modobj>.<param> gets into an error state.
|
|
||||||
If this method does not exist, and <param> is in autoupdate,
|
|
||||||
<modobj>.<param> gets into the same error state as self.<param>
|
|
||||||
"""
|
|
||||||
for pname in self.parameters:
|
|
||||||
errfunc = getattr(modobj, 'error_update_' + pname, None)
|
|
||||||
if errfunc:
|
|
||||||
def errcb(err, p=pname, efunc=errfunc):
|
|
||||||
try:
|
|
||||||
efunc(err)
|
|
||||||
except Exception as e:
|
|
||||||
modobj.announceUpdate(p, err=e)
|
|
||||||
self.errorCallbacks[pname].append(errcb)
|
|
||||||
else:
|
|
||||||
def errcb(err, p=pname):
|
|
||||||
modobj.announceUpdate(p, err=err)
|
|
||||||
if pname in autoupdate:
|
|
||||||
self.errorCallbacks[pname].append(errcb)
|
|
||||||
|
|
||||||
updfunc = getattr(modobj, 'update_' + pname, None)
|
Remark: when <modobj>.update_<param> does not accept the <exc> argument,
|
||||||
if updfunc:
|
nothing happens (the callback is catched by try / except).
|
||||||
def cb(value, ufunc=updfunc, efunc=errcb):
|
Any exceptions raised by the callback function are silently ignored.
|
||||||
try:
|
"""
|
||||||
ufunc(value)
|
autoupdate = set(autoupdate)
|
||||||
except Exception as e:
|
for pname in self.parameters:
|
||||||
efunc(e)
|
cbfunc = getattr(modobj, 'update_' + pname, None)
|
||||||
self.valueCallbacks[pname].append(cb)
|
if cbfunc:
|
||||||
|
self.addCallback(pname, cbfunc)
|
||||||
elif pname in autoupdate:
|
elif pname in autoupdate:
|
||||||
def cb(value, p=pname):
|
self.addCallback(pname, modobj.announceUpdate, pname)
|
||||||
modobj.announceUpdate(p, value)
|
|
||||||
self.valueCallbacks[pname].append(cb)
|
|
||||||
|
|
||||||
def isBusy(self, status=None):
|
def isBusy(self, status=None):
|
||||||
"""helper function for treating substates of BUSY correctly"""
|
"""helper function for treating substates of BUSY correctly"""
|
||||||
@ -614,6 +589,10 @@ class Module(HasAccessibles):
|
|||||||
# enablePoll == False: we still need the poll thread for writing values from writeDict
|
# enablePoll == False: we still need the poll thread for writing values from writeDict
|
||||||
if hasattr(self, 'io'):
|
if hasattr(self, 'io'):
|
||||||
self.io.polledModules.append(self)
|
self.io.polledModules.append(self)
|
||||||
|
if not self.io.triggerPoll:
|
||||||
|
# when self.io.enablePoll is False, triggerPoll is not
|
||||||
|
# created for self.io in the else clause below
|
||||||
|
self.triggerPoll = threading.Event()
|
||||||
else:
|
else:
|
||||||
self.triggerPoll = threading.Event()
|
self.triggerPoll = threading.Event()
|
||||||
self.polledModules.append(self)
|
self.polledModules.append(self)
|
||||||
@ -713,8 +692,8 @@ class Module(HasAccessibles):
|
|||||||
for mobj in polled_modules:
|
for mobj in polled_modules:
|
||||||
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
|
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
|
||||||
# trigger a poll interval change when self.pollinterval changes.
|
# trigger a poll interval change when self.pollinterval changes.
|
||||||
if 'pollinterval' in mobj.valueCallbacks:
|
if 'pollinterval' in mobj.paramCallbacks:
|
||||||
mobj.valueCallbacks['pollinterval'].append(pinfo.update_interval)
|
mobj.addCallback('pollinterval', pinfo.update_interval)
|
||||||
|
|
||||||
for pname, pobj in mobj.parameters.items():
|
for pname, pobj in mobj.parameters.items():
|
||||||
rfunc = getattr(mobj, 'read_' + pname)
|
rfunc = getattr(mobj, 'read_' + pname)
|
||||||
|
@ -36,6 +36,7 @@ from .modulebase import Module
|
|||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
"""basic readable module"""
|
"""basic readable module"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
Status = Enum('Status',
|
Status = Enum('Status',
|
||||||
IDLE=StatusType.IDLE,
|
IDLE=StatusType.IDLE,
|
||||||
WARN=StatusType.WARN,
|
WARN=StatusType.WARN,
|
||||||
@ -92,7 +93,7 @@ class Drivable(Writable):
|
|||||||
|
|
||||||
@Command(None, result=None)
|
@Command(None, result=None)
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""cease driving, go to IDLE state"""
|
"""not implemented - this is a no-op"""
|
||||||
|
|
||||||
|
|
||||||
class Communicator(HasComlog, Module):
|
class Communicator(HasComlog, Module):
|
||||||
|
@ -57,13 +57,17 @@ class Accessible(HasProperties):
|
|||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return self.propertyValues
|
return self.propertyValues
|
||||||
|
|
||||||
def override(self, value):
|
def create_from_value(self, properties, value):
|
||||||
"""override with a bare value"""
|
"""return a clone with given value and inherited properties"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def clone(self, properties, **kwds):
|
||||||
|
"""return a clone of ourselfs with inherited properties"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a (deep) copy of ourselfs"""
|
||||||
raise NotImplementedError
|
return self.clone(self.propertyValues)
|
||||||
|
|
||||||
def updateProperties(self, merged_properties):
|
def updateProperties(self, merged_properties):
|
||||||
"""update merged_properties with our own properties"""
|
"""update merged_properties with our own properties"""
|
||||||
@ -234,13 +238,15 @@ class Parameter(Accessible):
|
|||||||
# avoid export=True overrides export=<name>
|
# avoid export=True overrides export=<name>
|
||||||
self.ownProperties['export'] = self.export
|
self.ownProperties['export'] = self.export
|
||||||
|
|
||||||
def copy(self):
|
def clone(self, properties, **kwds):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a clone of ourselfs with inherited properties"""
|
||||||
res = type(self)()
|
res = type(self)(**kwds)
|
||||||
res.name = self.name
|
res.name = self.name
|
||||||
res.init(self.propertyValues)
|
res.init(properties)
|
||||||
|
res.init(res.ownProperties)
|
||||||
if 'datatype' in self.propertyValues:
|
if 'datatype' in self.propertyValues:
|
||||||
res.datatype = res.datatype.copy()
|
res.datatype = res.datatype.copy()
|
||||||
|
res.finish()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def updateProperties(self, merged_properties):
|
def updateProperties(self, merged_properties):
|
||||||
@ -253,9 +259,9 @@ class Parameter(Accessible):
|
|||||||
merged_properties.pop(key)
|
merged_properties.pop(key)
|
||||||
merged_properties.update(self.ownProperties)
|
merged_properties.update(self.ownProperties)
|
||||||
|
|
||||||
def override(self, value):
|
def create_from_value(self, properties, value):
|
||||||
"""override default"""
|
"""return a clone with given value and inherited properties"""
|
||||||
self.value = self.datatype(value)
|
return self.clone(properties, value=self.datatype(value))
|
||||||
|
|
||||||
def merge(self, merged_properties):
|
def merge(self, merged_properties):
|
||||||
"""merge with inherited properties
|
"""merge with inherited properties
|
||||||
@ -390,7 +396,7 @@ class Command(Accessible):
|
|||||||
else:
|
else:
|
||||||
# goodie: allow @Command instead of @Command()
|
# goodie: allow @Command instead of @Command()
|
||||||
self.func = argument # this is the wrapped method!
|
self.func = argument # this is the wrapped method!
|
||||||
if argument.__doc__:
|
if argument.__doc__ is not None:
|
||||||
self.description = inspect.cleandoc(argument.__doc__)
|
self.description = inspect.cleandoc(argument.__doc__)
|
||||||
self.name = self.func.__name__ # this is probably not needed
|
self.name = self.func.__name__ # this is probably not needed
|
||||||
self._inherit = inherit # save for __set_name__
|
self._inherit = inherit # save for __set_name__
|
||||||
@ -439,38 +445,37 @@ class Command(Accessible):
|
|||||||
f' members!: {params} != {members}')
|
f' members!: {params} != {members}')
|
||||||
self.argument.optional = [p for p,v in sig.parameters.items()
|
self.argument.optional = [p for p,v in sig.parameters.items()
|
||||||
if v.default is not inspect.Parameter.empty]
|
if v.default is not inspect.Parameter.empty]
|
||||||
if 'description' not in self.propertyValues and func.__doc__:
|
if 'description' not in self.ownProperties and func.__doc__ is not None:
|
||||||
self.description = inspect.cleandoc(func.__doc__)
|
self.description = inspect.cleandoc(func.__doc__)
|
||||||
self.ownProperties['description'] = self.description
|
self.ownProperties['description'] = self.description
|
||||||
self.func = func
|
self.func = func
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def copy(self):
|
def clone(self, properties, **kwds):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a clone of ourselfs with inherited properties"""
|
||||||
res = type(self)()
|
res = type(self)(**kwds)
|
||||||
res.name = self.name
|
res.name = self.name
|
||||||
res.func = self.func
|
res.func = self.func
|
||||||
res.init(self.propertyValues)
|
res.init(properties)
|
||||||
|
res.init(res.ownProperties)
|
||||||
if res.argument:
|
if res.argument:
|
||||||
res.argument = res.argument.copy()
|
res.argument = res.argument.copy()
|
||||||
if res.result:
|
if res.result:
|
||||||
res.result = res.result.copy()
|
res.result = res.result.copy()
|
||||||
self.finish()
|
res.finish()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def updateProperties(self, merged_properties):
|
def updateProperties(self, merged_properties):
|
||||||
"""update merged_properties with our own properties"""
|
"""update merged_properties with our own properties"""
|
||||||
merged_properties.update(self.ownProperties)
|
merged_properties.update(self.ownProperties)
|
||||||
|
|
||||||
def override(self, value):
|
def create_from_value(self, properties, value):
|
||||||
"""override method
|
"""return a clone with given value and inherited properties
|
||||||
|
|
||||||
this is needed when the @Command is missing on a method overriding a command"""
|
this is needed when the @Command is missing on a method overriding a command"""
|
||||||
if not callable(value):
|
if not callable(value):
|
||||||
raise ProgrammingError(f'{self.name} = {value!r} is overriding a Command')
|
raise ProgrammingError(f'{self.name} = {value!r} is overriding a Command')
|
||||||
self.func = value
|
return self.clone(properties)(value)
|
||||||
if value.__doc__:
|
|
||||||
self.description = inspect.cleandoc(value.__doc__)
|
|
||||||
|
|
||||||
def merge(self, merged_properties):
|
def merge(self, merged_properties):
|
||||||
"""merge with inherited properties
|
"""merge with inherited properties
|
||||||
|
@ -84,9 +84,7 @@ class PersistentMixin(Module):
|
|||||||
flag = getattr(pobj, 'persistent', False)
|
flag = getattr(pobj, 'persistent', False)
|
||||||
if flag:
|
if flag:
|
||||||
if flag == 'auto':
|
if flag == 'auto':
|
||||||
def cb(value, m=self):
|
self.addCallback(pname, self.saveParameters)
|
||||||
m.saveParameters()
|
|
||||||
self.valueCallbacks[pname].append(cb)
|
|
||||||
self.initData[pname] = pobj.value
|
self.initData[pname] = pobj.value
|
||||||
if not pobj.given:
|
if not pobj.given:
|
||||||
if pname in loaded:
|
if pname in loaded:
|
||||||
@ -129,12 +127,14 @@ class PersistentMixin(Module):
|
|||||||
self.writeInitParams()
|
self.writeInitParams()
|
||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
def saveParameters(self):
|
def saveParameters(self, _=None):
|
||||||
"""save persistent parameters
|
"""save persistent parameters
|
||||||
|
|
||||||
- to be called regularly explicitly by the module
|
- to be called regularly explicitly by the module
|
||||||
- the caller has to make sure that this is not called after
|
- the caller has to make sure that this is not called after
|
||||||
a power down of the connected hardware before loadParameters
|
a power down of the connected hardware before loadParameters
|
||||||
|
|
||||||
|
dummy argument to avoid closure for callback
|
||||||
"""
|
"""
|
||||||
if self.writeDict:
|
if self.writeDict:
|
||||||
# do not save before all values are written to the hw, as potentially
|
# do not save before all values are written to the hw, as potentially
|
||||||
|
@ -71,7 +71,7 @@ class ProxyModule(HasIO, Module):
|
|||||||
pname, pobj = params.popitem()
|
pname, pobj = params.popitem()
|
||||||
props = remoteparams.get(pname, None)
|
props = remoteparams.get(pname, None)
|
||||||
if props is None:
|
if props is None:
|
||||||
if pobj.export:
|
if pobj.export and pname != 'status':
|
||||||
self.log.warning('remote parameter %s:%s does not exist', self.module, pname)
|
self.log.warning('remote parameter %s:%s does not exist', self.module, pname)
|
||||||
continue
|
continue
|
||||||
dt = props['datatype']
|
dt = props['datatype']
|
||||||
@ -108,17 +108,19 @@ class ProxyModule(HasIO, Module):
|
|||||||
# for now, the error message must be enough
|
# for now, the error message must be enough
|
||||||
|
|
||||||
def nodeStateChange(self, online, state):
|
def nodeStateChange(self, online, state):
|
||||||
|
disconnected = Readable.Status.ERROR, 'disconnected'
|
||||||
if online:
|
if online:
|
||||||
if not self._consistency_check_done:
|
if not self._consistency_check_done:
|
||||||
self._check_descriptive_data()
|
self._check_descriptive_data()
|
||||||
self._consistency_check_done = True
|
self._consistency_check_done = True
|
||||||
|
if self.status == disconnected:
|
||||||
|
self.status = Readable.Status.IDLE, 'connected'
|
||||||
else:
|
else:
|
||||||
newstatus = Readable.Status.ERROR, 'disconnected'
|
|
||||||
readerror = CommunicationFailedError('disconnected')
|
readerror = CommunicationFailedError('disconnected')
|
||||||
if self.status != newstatus:
|
if self.status != disconnected:
|
||||||
for pname in set(self.parameters) - set(('module', 'status')):
|
for pname in set(self.parameters) - set(('module', 'status')):
|
||||||
self.announceUpdate(pname, None, readerror)
|
self.announceUpdate(pname, None, readerror)
|
||||||
self.announceUpdate('status', newstatus)
|
self.status = disconnected
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
pass # skip
|
pass # skip
|
||||||
@ -193,7 +195,7 @@ def proxy_class(remote_class, name=None):
|
|||||||
attrs[aname] = pobj
|
attrs[aname] = pobj
|
||||||
|
|
||||||
def rfunc(self, pname=aname):
|
def rfunc(self, pname=aname):
|
||||||
value, _, readerror = self._secnode.getParameter(self.name, pname, True)
|
value, _, readerror = self._secnode.getParameter(self.module, pname, True)
|
||||||
if readerror:
|
if readerror:
|
||||||
raise readerror
|
raise readerror
|
||||||
return value
|
return value
|
||||||
@ -203,7 +205,7 @@ def proxy_class(remote_class, name=None):
|
|||||||
if not pobj.readonly:
|
if not pobj.readonly:
|
||||||
|
|
||||||
def wfunc(self, value, pname=aname):
|
def wfunc(self, value, pname=aname):
|
||||||
value, _, readerror = self._secnode.setParameter(self.name, pname, value)
|
value, _, readerror = self._secnode.setParameter(self.module, pname, value)
|
||||||
if readerror:
|
if readerror:
|
||||||
raise readerror
|
raise readerror
|
||||||
return value
|
return value
|
||||||
@ -214,7 +216,7 @@ def proxy_class(remote_class, name=None):
|
|||||||
cobj = aobj.copy()
|
cobj = aobj.copy()
|
||||||
|
|
||||||
def cfunc(self, arg=None, cname=aname):
|
def cfunc(self, arg=None, cname=aname):
|
||||||
return self._secnode.execCommand(self.name, cname, arg)[0]
|
return self._secnode.execCommand(self.module, cname, arg)[0]
|
||||||
|
|
||||||
attrs[aname] = cobj(cfunc)
|
attrs[aname] = cobj(cfunc)
|
||||||
|
|
||||||
|
@ -239,6 +239,7 @@ class HasStates:
|
|||||||
|
|
||||||
@Command
|
@Command
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop state machine"""
|
||||||
self.stop_machine()
|
self.stop_machine()
|
||||||
|
|
||||||
def final_status(self, code=IDLE, text=''):
|
def final_status(self, code=IDLE, text=''):
|
||||||
|
@ -1,164 +0,0 @@
|
|||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# 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:
|
|
||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""convenience class to create a struct Parameter together with indivdual params
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
class Controller(Drivable):
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
ctrlpars = StructParam('ctrlpars struct', [
|
|
||||||
('pid_p', 'p', Parameter('control parameter p', FloatRange())),
|
|
||||||
('pid_i', 'i', Parameter('control parameter i', FloatRange())),
|
|
||||||
('pid_d', 'd', Parameter('control parameter d', FloatRange())),
|
|
||||||
], readonly=False)
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
then implement either read_ctrlpars and write_ctrlpars or
|
|
||||||
read_pid_p, read_pid_i, read_pid_d, write_pid_p, write_pid_i and write_pid_d
|
|
||||||
|
|
||||||
the methods not implemented will be created automatically
|
|
||||||
"""
|
|
||||||
|
|
||||||
from frappy.core import Parameter, Property
|
|
||||||
from frappy.datatypes import BoolType, DataType, StructOf, ValueType
|
|
||||||
from frappy.errors import ProgrammingError
|
|
||||||
|
|
||||||
|
|
||||||
class StructParam(Parameter):
|
|
||||||
"""create a struct parameter together with individual parameters
|
|
||||||
|
|
||||||
in addition to normal Parameter arguments:
|
|
||||||
|
|
||||||
:param paramdict: dict <member name> of Parameter(...)
|
|
||||||
:param prefix_or_map: either a prefix for the parameter name to add to the member name
|
|
||||||
or a dict <member name> or <paramerter name>
|
|
||||||
"""
|
|
||||||
# use properties, as simple attributes are not considered on copy()
|
|
||||||
paramdict = Property('dict <parametername> of Parameter(...)', ValueType())
|
|
||||||
hasStructRW = Property('has a read_<struct param> or write_<struct param> method',
|
|
||||||
BoolType(), default=False)
|
|
||||||
|
|
||||||
insideRW = 0 # counter for avoiding multiple superfluous updates
|
|
||||||
|
|
||||||
def __init__(self, description=None, paramdict=None, prefix_or_map='', *, datatype=None, readonly=False, **kwds):
|
|
||||||
if isinstance(paramdict, DataType):
|
|
||||||
raise ProgrammingError('second argument must be a dict of Param')
|
|
||||||
if datatype is None and paramdict is not None: # omit the following on Parameter.copy()
|
|
||||||
if isinstance(prefix_or_map, str):
|
|
||||||
prefix_or_map = {m: prefix_or_map + m for m in paramdict}
|
|
||||||
for membername, param in paramdict.items():
|
|
||||||
param.name = prefix_or_map[membername]
|
|
||||||
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
|
|
||||||
kwds['influences'] = [p.name for p in paramdict.values()]
|
|
||||||
self.updateEnable = {}
|
|
||||||
super().__init__(description, datatype, paramdict=paramdict, readonly=readonly, **kwds)
|
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
|
||||||
# names of access methods of structed param (e.g. ctrlpars)
|
|
||||||
struct_read_name = f'read_{name}' # e.g. 'read_ctrlpars'
|
|
||||||
struct_write_name = f'write_{name}' # e.h. 'write_ctrlpars'
|
|
||||||
self.hasStructRW = hasattr(owner, struct_read_name) or hasattr(owner, struct_write_name)
|
|
||||||
|
|
||||||
for membername, param in self.paramdict.items():
|
|
||||||
pname = param.name
|
|
||||||
changes = {
|
|
||||||
'readonly': self.readonly,
|
|
||||||
'influences': set(param.influences) | {name},
|
|
||||||
}
|
|
||||||
param.ownProperties.update(changes)
|
|
||||||
param.init(changes)
|
|
||||||
setattr(owner, pname, param)
|
|
||||||
param.__set_name__(owner, param.name)
|
|
||||||
|
|
||||||
if self.hasStructRW:
|
|
||||||
rname = f'read_{pname}'
|
|
||||||
|
|
||||||
if not hasattr(owner, rname):
|
|
||||||
def rfunc(self, membername=membername, struct_read_name=struct_read_name):
|
|
||||||
return getattr(self, struct_read_name)()[membername]
|
|
||||||
|
|
||||||
rfunc.poll = False # read_<struct param> is polled only
|
|
||||||
setattr(owner, rname, rfunc)
|
|
||||||
|
|
||||||
if not self.readonly:
|
|
||||||
wname = f'write_{pname}'
|
|
||||||
if not hasattr(owner, wname):
|
|
||||||
def wfunc(self, value, membername=membername,
|
|
||||||
name=name, rname=rname, struct_write_name=struct_write_name):
|
|
||||||
valuedict = dict(getattr(self, name))
|
|
||||||
valuedict[membername] = value
|
|
||||||
getattr(self, struct_write_name)(valuedict)
|
|
||||||
return getattr(self, rname)()
|
|
||||||
|
|
||||||
setattr(owner, wname, wfunc)
|
|
||||||
|
|
||||||
if not self.hasStructRW:
|
|
||||||
if not hasattr(owner, struct_read_name):
|
|
||||||
def struct_read_func(self, name=name, flist=tuple(
|
|
||||||
(m, f'read_{p.name}') for m, p in self.paramdict.items())):
|
|
||||||
pobj = self.parameters[name]
|
|
||||||
# disable updates generated from the callbacks of individual params
|
|
||||||
pobj.insideRW += 1 # guarded by self.accessLock
|
|
||||||
try:
|
|
||||||
return {m: getattr(self, f)() for m, f in flist}
|
|
||||||
finally:
|
|
||||||
pobj.insideRW -= 1
|
|
||||||
|
|
||||||
setattr(owner, struct_read_name, struct_read_func)
|
|
||||||
|
|
||||||
if not (self.readonly or hasattr(owner, struct_write_name)):
|
|
||||||
|
|
||||||
def struct_write_func(self, value, name=name, funclist=tuple(
|
|
||||||
(m, f'write_{p.name}') for m, p in self.paramdict.items())):
|
|
||||||
pobj = self.parameters[name]
|
|
||||||
pobj.insideRW += 1 # guarded by self.accessLock
|
|
||||||
try:
|
|
||||||
return {m: getattr(self, f)(value[m]) for m, f in funclist}
|
|
||||||
finally:
|
|
||||||
pobj.insideRW -= 1
|
|
||||||
|
|
||||||
setattr(owner, struct_write_name, struct_write_func)
|
|
||||||
|
|
||||||
super().__set_name__(owner, name)
|
|
||||||
|
|
||||||
def finish(self, modobj=None):
|
|
||||||
"""register callbacks for consistency"""
|
|
||||||
super().finish(modobj)
|
|
||||||
if modobj:
|
|
||||||
|
|
||||||
if self.hasStructRW:
|
|
||||||
def cb(value, modobj=modobj, structparam=self):
|
|
||||||
for membername, param in structparam.paramdict.items():
|
|
||||||
setattr(modobj, param.name, value[membername])
|
|
||||||
|
|
||||||
modobj.valueCallbacks[self.name].append(cb)
|
|
||||||
else:
|
|
||||||
for membername, param in self.paramdict.items():
|
|
||||||
def cb(value, modobj=modobj, structparam=self, membername=membername):
|
|
||||||
if not structparam.insideRW:
|
|
||||||
prev = dict(getattr(modobj, structparam.name))
|
|
||||||
prev[membername] = value
|
|
||||||
setattr(modobj, structparam.name, prev)
|
|
||||||
|
|
||||||
modobj.valueCallbacks[param.name].append(cb)
|
|
@ -120,6 +120,7 @@ class MagneticField(Drivable):
|
|||||||
)
|
)
|
||||||
heatswitch = Attached(Switch, description='name of heat switch device')
|
heatswitch = Attached(Switch, description='name of heat switch device')
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
||||||
|
|
||||||
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||||
@ -193,6 +194,7 @@ class MagneticField(Drivable):
|
|||||||
self.log.error(self, 'main thread exited unexpectedly!')
|
self.log.error(self, 'main thread exited unexpectedly!')
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current value"""
|
||||||
self.write_target(self.read_value())
|
self.write_target(self.read_value())
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,12 +36,12 @@ from time import sleep, time as currenttime
|
|||||||
import PyTango
|
import PyTango
|
||||||
|
|
||||||
from frappy.datatypes import ArrayOf, EnumType, FloatRange, IntRange, \
|
from frappy.datatypes import ArrayOf, EnumType, FloatRange, IntRange, \
|
||||||
LimitsType, StringType, TupleOf, ValueType
|
LimitsType, StatusType, StringType, TupleOf, ValueType
|
||||||
from frappy.errors import CommunicationFailedError, ConfigError, \
|
from frappy.errors import CommunicationFailedError, ConfigError, \
|
||||||
HardwareError, ProgrammingError, WrongTypeError
|
HardwareError, ProgrammingError, WrongTypeError
|
||||||
from frappy.lib import lazy_property
|
from frappy.lib import lazy_property
|
||||||
from frappy.modules import Command, Drivable, Module, Parameter, Readable, \
|
from frappy.modules import Command, Drivable, Module, Parameter, Property, \
|
||||||
StatusType, Writable, Property
|
Readable, Writable
|
||||||
|
|
||||||
#####
|
#####
|
||||||
|
|
||||||
@ -641,6 +641,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
sleep(0.3)
|
sleep(0.3)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""cease driving, go to IDLE state"""
|
||||||
self._dev.Stop()
|
self._dev.Stop()
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,19 +194,19 @@ class Nmr(Readable):
|
|||||||
x = val['xval'][:len(val['yval'])]
|
x = val['xval'][:len(val['yval'])]
|
||||||
return (x, val['yval'])
|
return (x, val['yval'])
|
||||||
|
|
||||||
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
@Command(IntRange(1), result=TupleOf(ArrayOf(string, maxlen=100),
|
||||||
ArrayOf(floating, maxlen=100)))
|
ArrayOf(floating, maxlen=100)))
|
||||||
def get_amplitude(self):
|
def get_amplitude(self, count):
|
||||||
"""Last 20 amplitude datapoints."""
|
"""Last <count> amplitude datapoints."""
|
||||||
rv = self.cell.cell.nmr_paramlog_get('amplitude', 20)
|
rv = self.cell.cell.nmr_paramlog_get('amplitude', count)
|
||||||
x = [ str(timestamp) for timestamp in rv['xval']]
|
x = [ str(timestamp) for timestamp in rv['xval']]
|
||||||
return (x,rv['yval'])
|
return (x,rv['yval'])
|
||||||
|
|
||||||
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
@Command(IntRange(1), result=TupleOf(ArrayOf(string, maxlen=100),
|
||||||
ArrayOf(floating, maxlen=100)))
|
ArrayOf(floating, maxlen=100)))
|
||||||
def get_phase(self):
|
def get_phase(self, count):
|
||||||
"""Last 20 phase datapoints."""
|
"""Last <count> phase datapoints."""
|
||||||
val = self.cell.cell.nmr_paramlog_get('phase', 20)
|
val = self.cell.cell.nmr_paramlog_get('phase', count)
|
||||||
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
||||||
|
|
||||||
|
|
||||||
|
229
frappy_psi/HP.py
229
frappy_psi/HP.py
@ -1,229 +0,0 @@
|
|||||||
#!/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 <oksana.shliakhtun@psi.ch>
|
|
||||||
# *****************************************************************************
|
|
||||||
"""Hewlett-Packard HP34401A Multimeter (not finished)"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from frappy.core import HasIO, Readable, Parameter, FloatRange, EnumType, StatusType, IDLE, ERROR, WARN
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_value(value):
|
|
||||||
"""
|
|
||||||
Converting the value to float, removing the units, converting the prefix into the number.
|
|
||||||
:param value: value
|
|
||||||
:return: float value without units
|
|
||||||
"""
|
|
||||||
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
|
||||||
value, pfx = value_with_unit.match(value).groups()
|
|
||||||
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
|
||||||
if pfx in pfx_dict:
|
|
||||||
value = round(float(value) * pfx_dict[pfx], 12)
|
|
||||||
return float(value)
|
|
||||||
|
|
||||||
|
|
||||||
class HP_IO(HasIO):
|
|
||||||
end_of_line = b'\n'
|
|
||||||
identification = [('*IDN?', r'HEWLETT-PACKARD,34401A,0,.*')]
|
|
||||||
|
|
||||||
|
|
||||||
class HP34401A(HP_IO):
|
|
||||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
|
||||||
autorange = Parameter('autorange_on', EnumType('autorange', off=0, on=1), readonly=False, default=0)
|
|
||||||
|
|
||||||
def comm(self, cmd): # read until \n
|
|
||||||
string = f'{cmd}\n'
|
|
||||||
n_string = string.encode()
|
|
||||||
response = self.communicate(n_string)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
|
|
||||||
response = self.communicate(n_string)
|
|
||||||
return response if response else None
|
|
||||||
|
|
||||||
def read_range(self, function):
|
|
||||||
return self.comm(f'{function}:range?')
|
|
||||||
|
|
||||||
def write_range(self, function, range):
|
|
||||||
return self.comm(f'{function}:range {range}')
|
|
||||||
|
|
||||||
def write_autorange(self, function):
|
|
||||||
cmd = f'{function}:range:auto {"on" if self.autorange == 0 else "off"}'
|
|
||||||
self.comm(cmd)
|
|
||||||
return self.comm(f'{function}:range:auto?')
|
|
||||||
|
|
||||||
def read_resolution(self, function):
|
|
||||||
return self.comm(f'{function}:resolution?')
|
|
||||||
|
|
||||||
def write_resolution(self, function, resolution):
|
|
||||||
self.comm(f'{function}:resolution {resolution}')
|
|
||||||
return self.comm(f'{function}:resolution?')
|
|
||||||
|
|
||||||
def read_status(self):
|
|
||||||
stb = int(self.comm('*STB?'))
|
|
||||||
esr = int(self.comm('*ESR?'))
|
|
||||||
|
|
||||||
if esr & (1 << 3):
|
|
||||||
return ERROR, 'self-test/calibration/reading failed'
|
|
||||||
if esr & (1 << 4):
|
|
||||||
return ERROR, 'execution error'
|
|
||||||
if esr & (1 << 5):
|
|
||||||
return ERROR, 'syntax error'
|
|
||||||
if esr & (1 << 2):
|
|
||||||
return ERROR, 'query error'
|
|
||||||
if stb & (1 << 3):
|
|
||||||
return WARN, 'questionable data'
|
|
||||||
if stb & (1 << 5):
|
|
||||||
return WARN, 'standard event register is not empty'
|
|
||||||
if stb & (1 << 6):
|
|
||||||
return WARN, 'requested service'
|
|
||||||
|
|
||||||
if any(stb & (1 << i) for i in range(3) or stb & (1 << 7)):
|
|
||||||
return IDLE, ''
|
|
||||||
if esr & (1 << 6):
|
|
||||||
return IDLE, ''
|
|
||||||
if esr & (1 << 7):
|
|
||||||
return IDLE, ''
|
|
||||||
if stb & (1 << 4):
|
|
||||||
return IDLE, 'message available'
|
|
||||||
if esr & (1 << 0):
|
|
||||||
return IDLE, 'operation complete'
|
|
||||||
if esr & (1 << 1):
|
|
||||||
return IDLE, 'not used'
|
|
||||||
|
|
||||||
|
|
||||||
class Voltage(HP34401A, Readable):
|
|
||||||
value = Parameter('voltage', datatype=FloatRange(0.1, 1000), unit='V')
|
|
||||||
range = Parameter('voltage sensitivity value', FloatRange(), unit='V', default=1, readonly=False)
|
|
||||||
resolution = Parameter('resolution')
|
|
||||||
mode = Parameter('measurement mode: ac/dc', readonly=False)
|
|
||||||
|
|
||||||
ioClass = HP_IO
|
|
||||||
|
|
||||||
MODE_NAMES = {0: 'dc', 1: 'ac'}
|
|
||||||
VOLT_RANGE = ['100mV', '1V', '10V', '100V', '1000V']
|
|
||||||
v_range = Parameter('voltage range', EnumType('voltage index range',
|
|
||||||
{name: idx for idx, name in enumerate(VOLT_RANGE)}), readonly=False)
|
|
||||||
|
|
||||||
acdc = None
|
|
||||||
|
|
||||||
def write_mode(self, mode):
|
|
||||||
"""
|
|
||||||
Set the mode - AC or DC
|
|
||||||
:param mode: AC/DC
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if mode == 1:
|
|
||||||
self.comm(f'configure:voltage:AC {self.range}, {self.resolution}')
|
|
||||||
else:
|
|
||||||
self.comm(f'configure:voltage:DC {self.range}, {self.resolution}')
|
|
||||||
self.acdc = self.MODE_NAMES[mode]
|
|
||||||
return self.comm(f'function?')
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
"""
|
|
||||||
Makes a AC/DC voltage measurement.
|
|
||||||
:return: AC/DC value
|
|
||||||
"""
|
|
||||||
return self.comm(f'measure:voltage:{self.acdc}?')
|
|
||||||
|
|
||||||
def write_autorange_acdc(self, function):
|
|
||||||
full_function = f'{function}:{self.acdc}'
|
|
||||||
return self.write_autorange(full_function)
|
|
||||||
|
|
||||||
def read_range_voltage(self):
|
|
||||||
return self.read_range(f'voltage:{self.acdc}')
|
|
||||||
|
|
||||||
def write_range_voltage(self, range):
|
|
||||||
return self.write_range(f'voltage:{self.acdc}', range)
|
|
||||||
|
|
||||||
def write_autorange_voltage(self):
|
|
||||||
return self.write_autorange_acdc('voltage')
|
|
||||||
|
|
||||||
def read_resolution_voltage(self):
|
|
||||||
return self.read_resolution(f'voltage:{self.acdc}')
|
|
||||||
|
|
||||||
def write_resolution_voltage(self, resolution):
|
|
||||||
return self.write_resolution(f'voltage:{self.acdc}', resolution)
|
|
||||||
|
|
||||||
|
|
||||||
class Current(HP34401A, Readable, Voltage):
|
|
||||||
value = Parameter('current', FloatRange, unit='A')
|
|
||||||
range = Parameter('current range', FloatRange)
|
|
||||||
CURR_RANGE_AC = ['10mA', '100mA', '1A', '3A']
|
|
||||||
CURR_RANGE_DC = ['1A', '3A']
|
|
||||||
|
|
||||||
def read_range_current(self):
|
|
||||||
return self.read_range(f'current:{self.acdc}')
|
|
||||||
|
|
||||||
def write_autorange_current(self):
|
|
||||||
return self.write_autorange_acdc('current')
|
|
||||||
|
|
||||||
def write_range_current(self, range):
|
|
||||||
return self.write_range(f'current:{self.acdc}', range)
|
|
||||||
|
|
||||||
def read_resolution_current(self):
|
|
||||||
return self.read_resolution(f'current:{self.acdc}')
|
|
||||||
|
|
||||||
def write_resolution_current(self, resolution):
|
|
||||||
return self.write_resolution(f'current:{self.acdc}', resolution)
|
|
||||||
|
|
||||||
|
|
||||||
class Resistance(HP34401A, Readable):
|
|
||||||
value = Parameter('resistance')
|
|
||||||
mode = Parameter('measurement mode: 2-/4-wire ohms', EnumType(two_wire=2, four_wire=4), readonly=False)
|
|
||||||
resolution = Parameter('resistance measurement resolution')
|
|
||||||
range = Parameter('resistance measurement range')
|
|
||||||
RESIST_RANGE = ['100Om', '1kOm', '10kOm', '100kOm', '1MOm', '10MOm', '100MOm']
|
|
||||||
FUNCTION_MAP = {2: 'resistance', 4: 'fresistance'}
|
|
||||||
|
|
||||||
def write_range_resistance(self, range):
|
|
||||||
return self.write_range(f'{self.FUNCTION_MAP[self.mode]}', range)
|
|
||||||
|
|
||||||
def read_range_resistance(self):
|
|
||||||
return self.read_range(f'{self.FUNCTION_MAP[self.mode]}')
|
|
||||||
|
|
||||||
def write_mode(self, mode):
|
|
||||||
if mode == 2:
|
|
||||||
self.comm(f'configure:resistance {self.range},{self.resolution}')
|
|
||||||
elif mode == 4:
|
|
||||||
self.comm(f'configure:fresistance {self.range}, {self.resolution}')
|
|
||||||
return self.comm('configure?')
|
|
||||||
|
|
||||||
def write_autorange_resistance(self):
|
|
||||||
return self.write_autorange(self.FUNCTION_MAP[self.mode])
|
|
||||||
|
|
||||||
def read_resolution_resistance(self):
|
|
||||||
return self.read_resolution(f'{self.FUNCTION_MAP[self.mode]}')
|
|
||||||
|
|
||||||
def write_resolution_resistance(self, resolution):
|
|
||||||
return self.write_resolution(f'{self.FUNCTION_MAP[self.mode]}', resolution)
|
|
||||||
|
|
||||||
|
|
||||||
class Frequency(HP34401A, Readable):
|
|
||||||
value = Parameter('frequency', FloatRange(3, 300e3), unit='Hz')
|
|
||||||
|
|
||||||
def write_autorange_frequency(self):
|
|
||||||
return self.write_autorange('frequency')
|
|
||||||
|
|
||||||
def read_resolution_frequency(self):
|
|
||||||
return self.read_resolution('frequency')
|
|
||||||
|
|
||||||
def write_resolution_frequency(self, resolution):
|
|
||||||
return self.write_resolution('frequency', resolution)
|
|
@ -15,7 +15,6 @@
|
|||||||
#
|
#
|
||||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Stanford Research Systems SR830 DS Lock-in Amplifier"""
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@ -26,11 +25,6 @@ from frappy.errors import IsBusyError
|
|||||||
|
|
||||||
|
|
||||||
def string_to_value(value):
|
def string_to_value(value):
|
||||||
"""
|
|
||||||
Converting the value to float, removing the units, converting the prefix into the number.
|
|
||||||
:param value: value
|
|
||||||
:return: float value without units
|
|
||||||
"""
|
|
||||||
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
||||||
value, pfx = value_with_unit.match(value).groups()
|
value, pfx = value_with_unit.match(value).groups()
|
||||||
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
||||||
@ -46,13 +40,6 @@ class SR830_IO(StringIO):
|
|||||||
|
|
||||||
class StanfRes(HasIO, Readable):
|
class StanfRes(HasIO, Readable):
|
||||||
def set_par(self, cmd, *args):
|
def set_par(self, cmd, *args):
|
||||||
"""
|
|
||||||
Set parameter.
|
|
||||||
Query commands are the same as setting commands, but they have a question mark.
|
|
||||||
:param cmd: command
|
|
||||||
:param args: value(s)
|
|
||||||
:return: reply
|
|
||||||
"""
|
|
||||||
head = ','.join([cmd] + [a if isinstance(a, str) else f'{a:g}' for a in args])
|
head = ','.join([cmd] + [a if isinstance(a, str) else f'{a:g}' for a in args])
|
||||||
tail = cmd.replace(' ', '? ')
|
tail = cmd.replace(' ', '? ')
|
||||||
new_tail = re.sub(r'[0-9.]+', '', tail)
|
new_tail = re.sub(r'[0-9.]+', '', tail)
|
||||||
@ -162,10 +149,6 @@ class XY(StanfRes):
|
|||||||
return IDLE, ''
|
return IDLE, ''
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
"""
|
|
||||||
Read XY. The manual autorange implemented.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self.read_status()[0] == BUSY:
|
if self.read_status()[0] == BUSY:
|
||||||
raise IsBusyError('changing gain')
|
raise IsBusyError('changing gain')
|
||||||
reply = self.get_par('SNAP? 1, 2')
|
reply = self.get_par('SNAP? 1, 2')
|
||||||
@ -183,13 +166,11 @@ class XY(StanfRes):
|
|||||||
return int(self.get_par('SENS?'))
|
return int(self.get_par('SENS?'))
|
||||||
|
|
||||||
def read_range(self):
|
def read_range(self):
|
||||||
"""Sensitivity range value"""
|
|
||||||
idx = self.read_irange()
|
idx = self.read_irange()
|
||||||
name = self.SEN_RANGE[idx]
|
name = self.SEN_RANGE[idx]
|
||||||
return string_to_value(name)
|
return string_to_value(name)
|
||||||
|
|
||||||
def write_irange(self, irange):
|
def write_irange(self, irange):
|
||||||
"""Index of sensitivity from the range"""
|
|
||||||
value = int(irange)
|
value = int(irange)
|
||||||
self.set_par(f'SENS {value}')
|
self.set_par(f'SENS {value}')
|
||||||
self._autogain_started = time.time()
|
self._autogain_started = time.time()
|
||||||
@ -197,12 +178,6 @@ class XY(StanfRes):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def write_range(self, target):
|
def write_range(self, target):
|
||||||
"""
|
|
||||||
Setting the sensitivity range.
|
|
||||||
cl_idx/cl_value is the closest index/value from the range to the target
|
|
||||||
:param target:
|
|
||||||
:return: closest value of the sensitivity range
|
|
||||||
"""
|
|
||||||
target = float(target)
|
target = float(target)
|
||||||
cl_idx = None
|
cl_idx = None
|
||||||
cl_value = float('inf')
|
cl_value = float('inf')
|
||||||
@ -219,7 +194,6 @@ class XY(StanfRes):
|
|||||||
return cl_value
|
return cl_value
|
||||||
|
|
||||||
def read_itc(self):
|
def read_itc(self):
|
||||||
"""Time constant index from the range"""
|
|
||||||
return int(self.get_par(f'OFLT?'))
|
return int(self.get_par(f'OFLT?'))
|
||||||
|
|
||||||
def write_itc(self, itc):
|
def write_itc(self, itc):
|
||||||
@ -227,18 +201,11 @@ class XY(StanfRes):
|
|||||||
return self.set_par(f'OFLT {value}')
|
return self.set_par(f'OFLT {value}')
|
||||||
|
|
||||||
def read_tc(self):
|
def read_tc(self):
|
||||||
"""Read time constant value from the range"""
|
|
||||||
idx = self.read_itc()
|
idx = self.read_itc()
|
||||||
name = self.TIME_CONST[idx]
|
name = self.TIME_CONST[idx]
|
||||||
return string_to_value(name)
|
return string_to_value(name)
|
||||||
|
|
||||||
def write_tc(self, target):
|
def write_tc(self, target):
|
||||||
"""
|
|
||||||
Setting the time constant from the range.
|
|
||||||
cl_idx/cl_value is the closest index/value from the range to the target
|
|
||||||
:param target: time constant
|
|
||||||
:return: closest time constant value
|
|
||||||
"""
|
|
||||||
target = float(target)
|
target = float(target)
|
||||||
cl_idx = None
|
cl_idx = None
|
||||||
cl_value = float('inf')
|
cl_value = float('inf')
|
||||||
|
@ -17,248 +17,690 @@
|
|||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from frappy.core import Drivable, Parameter, Command, Property, ERROR, WARN, BUSY, IDLE, Done, nopoll
|
import threading
|
||||||
from frappy.features import HasTargetLimits, HasSimpleOffset
|
from frappy.core import Drivable, Parameter, Command, Property, Module, HasIO, \
|
||||||
|
ERROR, WARN, BUSY, IDLE, nopoll, Limit
|
||||||
from frappy.datatypes import IntRange, FloatRange, StringType, BoolType
|
from frappy.datatypes import IntRange, FloatRange, StringType, BoolType
|
||||||
from frappy.errors import ConfigError, BadValueError
|
from frappy.errors import BadValueError, HardwareError, ConfigError
|
||||||
sys.path.append('/home/l_samenv/Documents/anc350/Linux64/userlib/lib')
|
|
||||||
from PyANC350v4 import Positioner
|
from PyANC350v4 import Positioner
|
||||||
|
|
||||||
|
|
||||||
DIRECTION_NAME = {1: 'forward', -1: 'backward'}
|
class IO(Module):
|
||||||
|
"""'communication' module for attocube controller
|
||||||
|
|
||||||
|
why an extra class:
|
||||||
class FreezeStatus:
|
- HasIO assures that a single common communicator is used
|
||||||
"""freeze status for some time
|
- access must be thread safe
|
||||||
|
|
||||||
hardware quite often does not treat status correctly: on a target change it
|
|
||||||
may take some time to return the 'busy' status correctly.
|
|
||||||
|
|
||||||
in classes with this mixin, within :meth:`write_target` call
|
|
||||||
|
|
||||||
self.freeze_status(0.5, BUSY, 'changed target')
|
|
||||||
|
|
||||||
a wrapper around read_status will take care that the status will be the given value,
|
|
||||||
for at least the given delay. This does NOT cover the case when self.status is set
|
|
||||||
directly from an other method.
|
|
||||||
"""
|
"""
|
||||||
__freeze_status_until = 0
|
uri = Property('dummy uri, only one controller may exists',
|
||||||
|
StringType())
|
||||||
|
export = False
|
||||||
|
_hw = None
|
||||||
|
_lock = None
|
||||||
|
used_axes = set()
|
||||||
|
|
||||||
def __init_subclass__(cls):
|
def initModule(self):
|
||||||
def wrapped(self, inner=cls.read_status):
|
if self._hw is None:
|
||||||
if time.time() < self.__freeze_status_until:
|
IO._lock = threading.Lock()
|
||||||
return Done
|
IO._hw = Positioner()
|
||||||
return inner(self)
|
super().initModule()
|
||||||
|
|
||||||
cls.read_status = wrapped
|
def shutdownModule(self):
|
||||||
super().__init_subclass__()
|
if IO._hw:
|
||||||
|
IO._hw.disconnect()
|
||||||
|
IO._hw = None
|
||||||
|
|
||||||
def freeze_status(self, delay, code=BUSY, text='changed target'):
|
def configureAQuadBIn(self, axisNo, enable, resolution):
|
||||||
"""freezze status to the given value for the given delay"""
|
"""Enables and configures the A-Quad-B (quadrature) input for the target position.
|
||||||
self.__freeze_status_until = time.time() + delay
|
Parameters
|
||||||
self.status = code, text
|
axisNo Axis number (0 ... 2)
|
||||||
|
enable Enable (1) or disable (0) A-Quad-B input
|
||||||
|
resolution A-Quad-B step width in m. Internal resolution is 1 nm.
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureAQuadBIn(axisNo, enable, resolution)
|
||||||
|
|
||||||
|
def configureAQuadBOut(self, axisNo, enable, resolution, clock):
|
||||||
|
"""Enables and configures the A-Quad-B output of the current position.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
enable Enable (1) or disable (0) A-Quad-B output
|
||||||
|
resolution A-Quad-B step width in m; internal resolution is 1 nm
|
||||||
|
clock Clock of the A-Quad-B output [s]. Allowed range is 40ns ... 1.3ms; internal resulution is 20ns.
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureAQuadBOut(axisNo, enable, resolution, clock)
|
||||||
|
|
||||||
|
def configureExtTrigger(self, axisNo, mode):
|
||||||
|
"""Enables the input trigger for steps.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
mode Disable (0), Quadratur (1), Trigger(2) for external triggering
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureExtTrigger(axisNo, mode)
|
||||||
|
|
||||||
|
def configureNslTriggerAxis(self, axisNo):
|
||||||
|
"""Selects Axis for NSL Trigger.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureNslTriggerAxis(axisNo)
|
||||||
|
|
||||||
|
def configureRngTrigger(self, axisNo, lower, upper):
|
||||||
|
"""Configure lower position for range Trigger.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
lower Lower position for range trigger (nm)
|
||||||
|
upper Upper position for range trigger (nm)
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureRngTrigger(axisNo, lower, upper)
|
||||||
|
|
||||||
|
def configureRngTriggerEps(self, axisNo, epsilon):
|
||||||
|
"""Configure hysteresis for range Trigger.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
epsilon hysteresis in nm / mdeg
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureRngTriggerEps(axisNo, epsilon)
|
||||||
|
|
||||||
|
def configureRngTriggerPol(self, axisNo, polarity):
|
||||||
|
"""Configure lower position for range Trigger.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
polarity Polarity of trigger signal when position is between lower and upper Low(0) and High(1)
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.configureRngTriggerPol(axisNo, polarity)
|
||||||
|
|
||||||
|
def getActuatorName(self, axisNo):
|
||||||
|
"""Get the name of the currently selected actuator
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
name Name of the actuator
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.getActuatorName(axisNo)
|
||||||
|
|
||||||
|
def getActuatorType(self, axisNo):
|
||||||
|
"""Get the type of the currently selected actuator
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
type_ Type of the actuator {0: linear, 1: goniometer, 2: rotator}
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.getActuatorType(axisNo)
|
||||||
|
|
||||||
|
def getAmplitude(self, axisNo):
|
||||||
|
"""Reads back the amplitude parameter of an axis.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
amplitude Amplitude V
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.getAmplitude(axisNo)
|
||||||
|
|
||||||
|
def getAxisStatus(self, axisNo):
|
||||||
|
"""Reads status information about an axis of the device.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
connected Output: If the axis is connected to a sensor.
|
||||||
|
enabled Output: If the axis voltage output is enabled.
|
||||||
|
moving Output: If the axis is moving.
|
||||||
|
target Output: If the target is reached in automatic positioning
|
||||||
|
eotFwd Output: If end of travel detected in forward direction.
|
||||||
|
eotBwd Output: If end of travel detected in backward direction.
|
||||||
|
error Output: If the axis' sensor is in error state.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.getAxisStatus(axisNo)
|
||||||
|
|
||||||
|
def getFrequency(self, axisNo):
|
||||||
|
"""Reads back the frequency parameter of an axis.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
frequency Output: Frequency in Hz
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.getFrequency(axisNo)
|
||||||
|
|
||||||
|
def getPosition(self, axisNo):
|
||||||
|
"""Retrieves the current actuator position. For linear type actuators the position unit is m; for goniometers and rotators it is degree.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
position Output: Current position [m] or [°]
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.getPosition(axisNo)
|
||||||
|
|
||||||
|
def measureCapacitance(self, axisNo):
|
||||||
|
"""Performs a measurement of the capacitance of the piezo motor and returns the result. If no motor is connected, the result will be 0. The function doesn't return before the measurement is complete; this will take a few seconds of time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
Returns
|
||||||
|
cap Output: Capacitance [F]
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.measureCapacitance(axisNo)
|
||||||
|
|
||||||
|
def selectActuator(self, axisNo, actuator):
|
||||||
|
"""Selects the actuator to be used for the axis from actuator presets.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
actuator Actuator selection (0 ... 255)
|
||||||
|
0: ANPg101res
|
||||||
|
1: ANGt101res
|
||||||
|
2: ANPx51res
|
||||||
|
3: ANPx101res
|
||||||
|
4: ANPx121res
|
||||||
|
5: ANPx122res
|
||||||
|
6: ANPz51res
|
||||||
|
7: ANPz101res
|
||||||
|
8: ANR50res
|
||||||
|
9: ANR51res
|
||||||
|
10: ANR101res
|
||||||
|
11: Test
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.selectActuator(axisNo, actuator)
|
||||||
|
|
||||||
|
def setAmplitude(self, axisNo, amplitude):
|
||||||
|
"""Sets the amplitude parameter for an axis
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
amplitude Amplitude in V, internal resolution is 1 mV
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.setAmplitude(axisNo, amplitude)
|
||||||
|
|
||||||
|
def setAxisOutput(self, axisNo, enable, autoDisable):
|
||||||
|
"""Enables or disables the voltage output of an axis.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
enable Enables (1) or disables (0) the voltage output.
|
||||||
|
autoDisable If the voltage output is to be deactivated automatically when end of travel is detected.
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.setAxisOutput(axisNo, enable, autoDisable)
|
||||||
|
|
||||||
|
def setDcVoltage(self, axisNo, voltage):
|
||||||
|
"""Sets the DC level on the voltage output when no sawtooth based motion is active.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
voltage DC output voltage [V], internal resolution is 1 mV
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.setDcVoltage(axisNo, voltage)
|
||||||
|
|
||||||
|
def setFrequency(self, axisNo, frequency):
|
||||||
|
"""Sets the frequency parameter for an axis
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
frequency Frequency in Hz, internal resolution is 1 Hz
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.setFrequency(axisNo, frequency)
|
||||||
|
|
||||||
|
def setTargetPosition(self, axisNo, target):
|
||||||
|
"""Sets the target position for automatic motion, see ANC_startAutoMove. For linear type actuators the position unit is m, for goniometers and rotators it is degree.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
target Target position [m] or [°]. Internal resulution is 1 nm or 1 µ°.
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.setTargetPosition(axisNo, target)
|
||||||
|
|
||||||
|
def setTargetRange(self, axisNo, targetRg):
|
||||||
|
"""Defines the range around the target position where the target is considered to be reached.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
targetRg Target range [m] or [°]. Internal resulution is 1 nm or 1 µ°.
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.setTargetRange(axisNo, targetRg)
|
||||||
|
|
||||||
|
def startAutoMove(self, axisNo, enable, relative):
|
||||||
|
"""Switches automatic moving (i.e. following the target position) on or off
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
enable Enables (1) or disables (0) automatic motion
|
||||||
|
relative If the target position is to be interpreted absolute (0) or relative to the current position (1)
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.startAutoMove(axisNo, enable, relative)
|
||||||
|
|
||||||
|
def startContinuousMove(self, axisNo, start, backward):
|
||||||
|
"""Starts or stops continous motion in forward direction. Other kinds of motions are stopped.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
start Starts (1) or stops (0) the motion
|
||||||
|
backward If the move direction is forward (0) or backward (1)
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.startContinuousMove(axisNo, start, backward)
|
||||||
|
|
||||||
|
def startSingleStep(self, axisNo, backward):
|
||||||
|
"""Triggers a single step in desired direction.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
axisNo Axis number (0 ... 2)
|
||||||
|
backward If the step direction is forward (0) or backward (1)
|
||||||
|
Returns
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._hw.startSingleStep(axisNo, backward)
|
||||||
|
|
||||||
|
|
||||||
class Axis(HasTargetLimits, FreezeStatus, Drivable):
|
class Stopped(RuntimeError):
|
||||||
|
"""thread was stopped"""
|
||||||
|
|
||||||
|
|
||||||
|
class Axis(HasIO, Drivable):
|
||||||
axis = Property('axis number', IntRange(0, 2), 0)
|
axis = Property('axis number', IntRange(0, 2), 0)
|
||||||
value = Parameter('axis position', FloatRange(unit='deg'))
|
value = Parameter('axis position', FloatRange(unit='deg'))
|
||||||
frequency = Parameter('frequency', FloatRange(1, unit='Hz'), readonly=False)
|
frequency = Parameter('frequency', FloatRange(1, unit='Hz'), readonly=False)
|
||||||
amplitude = Parameter('amplitude', FloatRange(0, unit='V'), readonly=False)
|
amplitude = Parameter('amplitude', FloatRange(0, unit='V'), readonly=False)
|
||||||
gear = Parameter('gear factor', FloatRange(), readonly=False, default=1, initwrite=True)
|
gear = Parameter('gear factor', FloatRange(), readonly=False, value=1)
|
||||||
tolerance = Parameter('positioning tolerance', FloatRange(0, unit='$'), readonly=False, default=0.01)
|
tolerance = Parameter('positioning tolerance', FloatRange(0, unit='$'),
|
||||||
output = Parameter('enable output', BoolType(), readonly=False)
|
readonly=False, default=0.01)
|
||||||
|
sensor_connected = Parameter('a sensor is connected', BoolType())
|
||||||
info = Parameter('axis info', StringType())
|
info = Parameter('axis info', StringType())
|
||||||
statusbits = Parameter('status bits', StringType())
|
statusbits = Parameter('status bits', StringType())
|
||||||
|
step_mode = Parameter('step mode (soft closed loop)', BoolType(),
|
||||||
|
default=False, readonly=False, group='step_mode')
|
||||||
|
timeout = Parameter('timeout after no progress detected', FloatRange(0),
|
||||||
|
default=1, readonly=False, group='step_mode')
|
||||||
|
steps_fwd = Parameter('forward steps / main unit', FloatRange(0), unit='$/s',
|
||||||
|
default=0, readonly=False, group='step_mode')
|
||||||
|
steps_bwd = Parameter('backward steps / main unit', FloatRange(0, unit='$/s'),
|
||||||
|
default=0, readonly=False, group='step_mode')
|
||||||
|
delay = Parameter('delay between tries within loop', FloatRange(0, unit='s'),
|
||||||
|
readonly=False, default=0.05, group='step_mode')
|
||||||
|
maxstep = Parameter('max. step duration', FloatRange(0, unit='s'),
|
||||||
|
default=0.25, readonly=False, group='step_mode')
|
||||||
|
prop = Parameter('factor for control loop', FloatRange(0, 1),
|
||||||
|
readonly=False, default=0.8, group='step_mode')
|
||||||
|
uri = 'ANC'
|
||||||
|
ioClass = IO
|
||||||
|
target_min = Limit()
|
||||||
|
target_max = Limit()
|
||||||
|
|
||||||
_hw = Positioner()
|
fast_interval = 0.25
|
||||||
|
|
||||||
|
_hw = None
|
||||||
_scale = 1 # scale for custom units
|
_scale = 1 # scale for custom units
|
||||||
_move_steps = 0 # number of steps to move (used by move command)
|
|
||||||
SCALES = {'deg': 1, 'm': 1, 'mm': 1000, 'um': 1000000, 'µm': 1000000}
|
SCALES = {'deg': 1, 'm': 1, 'mm': 1000, 'um': 1000000, 'µm': 1000000}
|
||||||
_direction = 1 # move direction
|
_thread = None
|
||||||
_idle_status = IDLE, ''
|
_moving_since = 0
|
||||||
_error_state = '' # empty string: no error
|
_status = IDLE, ''
|
||||||
_history = None
|
_calib_range = None
|
||||||
_check_sensor = False
|
_try_cnt = 0
|
||||||
_try_count = 0
|
_at_target = False
|
||||||
|
|
||||||
def __init__(self, name, logger, opts, srv):
|
def initModule(self):
|
||||||
unit = opts.pop('unit', 'deg')
|
super().initModule()
|
||||||
opts['value.unit'] = unit
|
self._stopped = threading.Event()
|
||||||
|
if self.axis in IO.used_axes:
|
||||||
|
raise ConfigError(f'a module with axisNo={self.axis} already exists')
|
||||||
|
IO.used_axes.add(self.axis)
|
||||||
|
|
||||||
|
def initialReads(self):
|
||||||
|
self.read_info()
|
||||||
|
super().initialReads()
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
IO.used_axes.discard(self.axis)
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self._thread:
|
||||||
|
return self.value
|
||||||
try:
|
try:
|
||||||
self._scale = self.SCALES[unit] * opts.get('gear', 1)
|
return self._read_pos()
|
||||||
except KeyError as e:
|
except Stopped:
|
||||||
raise ConfigError('unsupported unit: %s' % unit)
|
return self.value
|
||||||
super().__init__(name, logger, opts, srv)
|
|
||||||
|
|
||||||
def write_gear(self, value):
|
def write_gear(self, value):
|
||||||
self._scale = self.SCALES[self.parameters['value'].datatype.unit] * self.gear
|
self._scale = self.SCALES[self.parameters['value'].datatype.unit] * self.gear
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def startModule(self, start_events):
|
|
||||||
super().startModule(start_events)
|
|
||||||
start_events.queue(self.read_info)
|
|
||||||
|
|
||||||
def check_value(self, value):
|
|
||||||
"""check if value allows moving in current direction"""
|
|
||||||
if self._direction > 0:
|
|
||||||
if value > self.target_limits[1]:
|
|
||||||
raise BadValueError('above upper limit')
|
|
||||||
elif value < self.target_limits[0]:
|
|
||||||
raise BadValueError('below lower limit')
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
pos = self._hw.getPosition(self.axis) * self._scale
|
|
||||||
if self.isBusy():
|
|
||||||
try:
|
|
||||||
self.check_value(pos)
|
|
||||||
except BadValueError as e:
|
|
||||||
self._stop()
|
|
||||||
self._idle_status = ERROR, str(e)
|
|
||||||
return pos
|
|
||||||
|
|
||||||
def read_frequency(self):
|
def read_frequency(self):
|
||||||
return self._hw.getFrequency(self.axis)
|
return self.io.getFrequency(self.axis)
|
||||||
|
|
||||||
def write_frequency(self, value):
|
def write_frequency(self, value):
|
||||||
self._hw.setFrequency(self.axis, value)
|
self.io.setFrequency(self.axis, value)
|
||||||
return self._hw.getFrequency(self.axis)
|
return self.io.getFrequency(self.axis)
|
||||||
|
|
||||||
def read_amplitude(self):
|
def read_amplitude(self):
|
||||||
return self._hw.getAmplitude(self.axis)
|
return self.io.getAmplitude(self.axis)
|
||||||
|
|
||||||
def write_amplitude(self, value):
|
def write_amplitude(self, value):
|
||||||
self._hw.setAmplitude(self.axis, value)
|
self.io.setAmplitude(self.axis, value)
|
||||||
return self._hw.getAmplitude(self.axis)
|
return self.io.getAmplitude(self.axis)
|
||||||
|
|
||||||
def write_tolerance(self, value):
|
def read_statusbits(self):
|
||||||
self._hw.setTargetRange(self.axis, value / self._scale)
|
self._get_status()
|
||||||
return value
|
return self.statusbits
|
||||||
|
|
||||||
def write_output(self, value):
|
def _get_status(self):
|
||||||
self._hw.setAxisOutput(self.axis, enable=value, autoDisable=0)
|
"""get axis status
|
||||||
return value
|
|
||||||
|
- update self.sensor_connected and self.statusbits
|
||||||
|
- return <moving flag>, <error flag>, <reason>
|
||||||
|
|
||||||
|
<moving flag> is True whn moving
|
||||||
|
<in_error> is True when in error
|
||||||
|
<reason> is an error text, when in error, 'at target' or '' otherwise
|
||||||
|
"""
|
||||||
|
statusbits = self.io.getAxisStatus(self.axis)
|
||||||
|
self.sensor_connected, self._output, moving, at_target, fwd_stuck, bwd_stuck, error = statusbits
|
||||||
|
self.statusbits = ''.join(k for k, v in zip('OTFBE', (self._output,) + statusbits[3:]) if v)
|
||||||
|
if error:
|
||||||
|
return ERROR, 'other error'
|
||||||
|
if bwd_stuck:
|
||||||
|
return ERROR, 'end of travel backward'
|
||||||
|
if fwd_stuck:
|
||||||
|
return ERROR, 'end of travel forward'
|
||||||
|
target_reached = at_target > self._at_target
|
||||||
|
self._at_target = at_target
|
||||||
|
if self._moving_since:
|
||||||
|
if target_reached:
|
||||||
|
return IDLE, 'at target'
|
||||||
|
if time.time() < self._moving_since + 0.25:
|
||||||
|
return BUSY, 'started'
|
||||||
|
if at_target:
|
||||||
|
return IDLE, 'at target'
|
||||||
|
if moving and self._output:
|
||||||
|
return BUSY, 'moving'
|
||||||
|
return WARN, 'stopped by unknown reason'
|
||||||
|
if self._moving_since is False:
|
||||||
|
return IDLE, 'stopped'
|
||||||
|
if not self.step_mode and at_target:
|
||||||
|
return IDLE, 'at target'
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
statusbits = self._hw.getAxisStatus(self.axis)
|
status = self._get_status()
|
||||||
sensor, self.output, moving, attarget, eot_fwd, eot_bwd, sensor_error = statusbits
|
if self.step_mode:
|
||||||
self.statusbits = ''.join((k for k, v in zip('SOMTFBE', statusbits) if v))
|
return self._status
|
||||||
if self._move_steps:
|
if self._moving_since:
|
||||||
if not (eot_fwd or eot_bwd):
|
if status[0] != BUSY:
|
||||||
return BUSY, 'moving by steps'
|
self._moving_since = 0
|
||||||
if not sensor:
|
self.setFastPoll(False)
|
||||||
self._error_state = 'no sensor connected'
|
return status
|
||||||
elif sensor_error:
|
|
||||||
self._error_state = 'sensor error'
|
def _wait(self, delay):
|
||||||
elif eot_fwd:
|
if self._stopped.wait(delay):
|
||||||
self._error_state = 'end of travel forward'
|
raise Stopped()
|
||||||
elif eot_bwd:
|
|
||||||
self._error_state = 'end of travel backward'
|
def _read_pos(self):
|
||||||
|
if not self.sensor_connected:
|
||||||
|
return 0
|
||||||
|
poslist = []
|
||||||
|
for i in range(9):
|
||||||
|
if i:
|
||||||
|
self._wait(0.001)
|
||||||
|
poslist.append(self.io.getPosition(self.axis) * self._scale)
|
||||||
|
self._poslist = sorted(poslist)
|
||||||
|
return self._poslist[len(poslist) // 2] # median
|
||||||
|
|
||||||
|
def _run_drive(self, target):
|
||||||
|
self.value = self._read_pos()
|
||||||
|
self.status = self._status = BUSY, 'drive by steps'
|
||||||
|
deadline = time.time() + self.timeout
|
||||||
|
max_steps = self.maxstep * self.frequency
|
||||||
|
while True:
|
||||||
|
for _ in range(2):
|
||||||
|
dif = target - self.value
|
||||||
|
steps_per_unit = self.steps_bwd if dif < 0 else self.steps_fwd
|
||||||
|
tol = max(self.tolerance, 0.6 / steps_per_unit) # avoid a tolerance less than 60% of a step
|
||||||
|
if abs(dif) > tol * 3:
|
||||||
|
break
|
||||||
|
# extra wait time when already close
|
||||||
|
self._wait(2 * self.delay)
|
||||||
|
self.read_value()
|
||||||
|
status = None
|
||||||
|
if abs(dif) < tol:
|
||||||
|
status = IDLE, 'in tolerance'
|
||||||
|
elif self._poslist[2] <= target <= self._poslist[-3]: # target within noise
|
||||||
|
status = IDLE, 'within noise'
|
||||||
|
elif dif > 0:
|
||||||
|
steps = min(max_steps, min(dif, (dif + tol) * self.prop) * steps_per_unit)
|
||||||
else:
|
else:
|
||||||
if self._error_state and not DIRECTION_NAME[self._direction] in self._error_state:
|
steps = max(-max_steps, max(dif, (dif - tol) * self.prop) * steps_per_unit)
|
||||||
self._error_state = ''
|
if status or steps == 0:
|
||||||
status_text = 'moving' if self._try_count == 0 else 'moving (retry %d)' % self._try_count
|
self._status = status
|
||||||
if moving and self._history is not None: # history None: moving by steps
|
break
|
||||||
self._history.append(self.value)
|
if round(steps) == 0: # this should not happen
|
||||||
if len(self._history) < 5:
|
self._status = WARN, 'steps=0'
|
||||||
return BUSY, status_text
|
break
|
||||||
beg = self._history.pop(0)
|
self._move_steps(steps)
|
||||||
if abs(beg - self.target) < self.tolerance:
|
if self._step_size > self.prop * 0.25 / steps_per_unit:
|
||||||
# reset normal tolerance
|
# some progress happened
|
||||||
self._stop()
|
deadline = time.time() + self.timeout
|
||||||
self._idle_status = IDLE, 'in tolerance'
|
elif time.time() > deadline:
|
||||||
return self._idle_status
|
self._status = WARN, 'timeout - no progress'
|
||||||
# self._hw.setTargetRange(self.axis, self.tolerance / self._scale)
|
break
|
||||||
if (self.value - beg) * self._direction > 0:
|
self.read_status()
|
||||||
return BUSY, status_text
|
|
||||||
self._try_count += 1
|
|
||||||
if self._try_count < 10:
|
|
||||||
self.log.warn('no progress retry %d', self._try_count)
|
|
||||||
return BUSY, status_text
|
|
||||||
self._idle_status = WARN, 'no progress'
|
|
||||||
if self._error_state:
|
|
||||||
self._try_count += 1
|
|
||||||
if self._try_count < 10 and self._history is not None:
|
|
||||||
self.log.warn('end of travel retry %d', self._try_count)
|
|
||||||
self.write_target(self.target)
|
|
||||||
return Done
|
|
||||||
self._idle_status = WARN, self._error_state
|
|
||||||
if self.status[0] != IDLE:
|
|
||||||
self._stop()
|
|
||||||
return self._idle_status
|
|
||||||
|
|
||||||
def write_target(self, value):
|
def _thread_wrapper(self, func, *args):
|
||||||
if value == self.read_value():
|
|
||||||
return value
|
|
||||||
self.check_limits(value)
|
|
||||||
self._try_count = 0
|
|
||||||
self._direction = 1 if value > self.value else -1
|
|
||||||
# if self._error_state and DIRECTION_NAME[-self._direction] not in self._error_state:
|
|
||||||
# raise BadValueError('can not move (%s)' % self._error_state)
|
|
||||||
self._move_steps = 0
|
|
||||||
self.write_output(1)
|
|
||||||
# try first with 50 % of tolerance
|
|
||||||
self._hw.setTargetRange(self.axis, self.tolerance * 0.5 / self._scale)
|
|
||||||
for itry in range(5):
|
|
||||||
try:
|
try:
|
||||||
self._hw.setTargetPosition(self.axis, value / self._scale)
|
func(*args)
|
||||||
self._hw.startAutoMove(self.axis, enable=1, relative=0)
|
except Stopped as e:
|
||||||
|
self._status = IDLE, str(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if itry == 4:
|
self._status = ERROR, f'{type(e).__name__} - {e}'
|
||||||
raise
|
finally:
|
||||||
self.log.warn('%r', e)
|
self.io.setAxisOutput(self.axis, enable=0, autoDisable=0)
|
||||||
self._history = [self.value]
|
self.setFastPoll(False)
|
||||||
self._idle_status = IDLE, ''
|
self._stopped.clear()
|
||||||
self.freeze_status(1, BUSY, 'changed target')
|
self._thread = None
|
||||||
self.setFastPoll(True, 1)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def doPoll(self):
|
def _stop_thread(self):
|
||||||
if self._move_steps == 0:
|
if self._thread:
|
||||||
super().doPoll()
|
self._stopped.set()
|
||||||
|
self._thread.join()
|
||||||
|
|
||||||
|
def _start_thread(self, *args):
|
||||||
|
self._stop_thread()
|
||||||
|
thread = threading.Thread(target=self._thread_wrapper, args=args)
|
||||||
|
self._thread = thread
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if not self.sensor_connected:
|
||||||
|
raise HardwareError('no sensor connected')
|
||||||
|
self._stop_thread()
|
||||||
|
self.io.setTargetRange(self.axis, self.tolerance / self._scale)
|
||||||
|
if self.step_mode:
|
||||||
|
self.status = BUSY, 'changed target'
|
||||||
|
self._start_thread(self._run_drive, target)
|
||||||
|
else:
|
||||||
|
self._try_cnt = 0
|
||||||
|
self.setFastPoll(True, self.fast_interval)
|
||||||
|
self.io.setTargetPosition(self.axis, target / self._scale)
|
||||||
|
self.io.setAxisOutput(self.axis, enable=1, autoDisable=0)
|
||||||
|
self.io.startAutoMove(self.axis, enable=1, relative=0)
|
||||||
|
self._moving_since = time.time()
|
||||||
|
self.status = self._get_status()
|
||||||
|
return target
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def stop(self):
|
||||||
|
if self.step_mode:
|
||||||
|
self._stop_thread()
|
||||||
|
self._status = IDLE, 'stopped'
|
||||||
|
elif self._moving_since:
|
||||||
|
self._moving_since = False # indicate stop
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
@Command(IntRange())
|
||||||
|
def move(self, steps):
|
||||||
|
"""relative move by number of steps"""
|
||||||
|
self._stop_thread()
|
||||||
|
self.read_value()
|
||||||
|
if steps > 0:
|
||||||
|
if self.value > self.target_max:
|
||||||
|
raise BadValueError('above upper limit')
|
||||||
|
elif self.value < self.target_min:
|
||||||
|
raise BadValueError('below lower limit')
|
||||||
|
self.status = self._status = BUSY, 'moving relative'
|
||||||
|
self._start_thread(self._run_move, steps)
|
||||||
|
|
||||||
|
def _run_move(self, steps):
|
||||||
|
self.setFastPoll(True, self.fast_interval)
|
||||||
|
self._move_steps(steps)
|
||||||
|
self.status = self._status = IDLE, ''
|
||||||
|
|
||||||
|
def _move_steps(self, steps):
|
||||||
|
steps = round(steps)
|
||||||
|
if not steps:
|
||||||
return
|
return
|
||||||
self._hw.startSingleStep(self.axis, self._direction < 0)
|
previous = self._read_pos()
|
||||||
self._move_steps -= self._direction
|
self.io.setAxisOutput(self.axis, enable=1, autoDisable=0)
|
||||||
if self._move_steps % int(self.frequency) == 0: # poll value and status every second
|
# wait for output is really on
|
||||||
super().doPoll()
|
for i in range(100):
|
||||||
|
self._wait(0.001)
|
||||||
|
self._get_status()
|
||||||
|
if self._output:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError('can not switch on output')
|
||||||
|
for cnt in range(abs(steps)):
|
||||||
|
self.io.setAxisOutput(self.axis, enable=1, autoDisable=0)
|
||||||
|
if not self._thread:
|
||||||
|
raise Stopped('stopped')
|
||||||
|
self.io.startSingleStep(self.axis, steps < 0)
|
||||||
|
self._wait(1 / self.frequency)
|
||||||
|
self._get_status()
|
||||||
|
if cnt and not self._output:
|
||||||
|
steps = cnt
|
||||||
|
break
|
||||||
|
self._wait(self.delay)
|
||||||
|
self.value = self._read_pos()
|
||||||
|
self._step_size = (self.value - previous) / steps
|
||||||
|
|
||||||
|
@Command(IntRange(0))
|
||||||
|
def calib_steps(self, delta):
|
||||||
|
"""calibrate steps_fwd and steps_bwd using <delta> steps forwards and backwards"""
|
||||||
|
if not self.sensor_connected:
|
||||||
|
raise HardwareError('no sensor connected')
|
||||||
|
self._stop_thread()
|
||||||
|
self._status = BUSY, 'calibrate step size'
|
||||||
|
self.read_status()
|
||||||
|
self._start_thread(self._run_calib, delta)
|
||||||
|
|
||||||
|
def _run_calib(self, steps):
|
||||||
|
self.value = self._read_pos()
|
||||||
|
if self._calib_range is None or abs(self.target - self.value) > self._calib_range:
|
||||||
|
self.target = self.value
|
||||||
|
maxfwd = 0
|
||||||
|
maxbwd = 0
|
||||||
|
cntfwd = 0
|
||||||
|
cntbwd = 0
|
||||||
|
self._calib_range = 0
|
||||||
|
for i in range(10):
|
||||||
|
if self.value <= self.target:
|
||||||
|
self._status = BUSY, 'move forwards'
|
||||||
|
self.read_status()
|
||||||
|
self._move_steps(steps)
|
||||||
|
while True:
|
||||||
|
self._move_steps(steps)
|
||||||
|
if self._step_size and self._output:
|
||||||
|
maxfwd = max(maxfwd, self._step_size)
|
||||||
|
cntfwd += 1
|
||||||
|
if self.value > self.target:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._status = BUSY, 'move backwards'
|
||||||
|
self.read_status()
|
||||||
|
self._move_steps(-steps)
|
||||||
|
while True:
|
||||||
|
self._move_steps(-steps)
|
||||||
|
if self._step_size:
|
||||||
|
maxbwd = max(maxbwd, self._step_size)
|
||||||
|
cntbwd += 1
|
||||||
|
if self.value < self.target:
|
||||||
|
break
|
||||||
|
# keep track how far we had to go for calibration
|
||||||
|
self._calib_range = max(self._calib_range, abs(self.value - self.target))
|
||||||
|
if cntfwd >= 3 and cntbwd >= 3:
|
||||||
|
self.steps_fwd = 1 / maxfwd
|
||||||
|
self.steps_bwd = 1 / maxbwd
|
||||||
|
self._status = IDLE, 'calib step size done'
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._status = WARN, 'calib step size failed'
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
@nopoll
|
@nopoll
|
||||||
def read_info(self):
|
def read_info(self):
|
||||||
"""read info from controller"""
|
"""read info from controller"""
|
||||||
cap = self._hw.measureCapacitance(self.axis) * 1e9
|
axistype = ['linear', 'gonio', 'rotator'][self.io.getActuatorType(self.axis)]
|
||||||
axistype = ['linear', 'gonio', 'rotator'][self._hw.getActuatorType(self.axis)]
|
name = self.io.getActuatorName(self.axis)
|
||||||
return '%s %s %.3gnF' % (self._hw.getActuatorName(self.axis), axistype, cap)
|
cap = self.io.measureCapacitance(self.axis) * 1e9
|
||||||
|
return f'{name} {axistype} {cap:.3g}nF'
|
||||||
def _stop(self):
|
|
||||||
self._move_steps = 0
|
|
||||||
self._history = None
|
|
||||||
for _ in range(5):
|
|
||||||
try:
|
|
||||||
self._hw.startAutoMove(self.axis, enable=0, relative=0)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
if itry == 4:
|
|
||||||
raise
|
|
||||||
self.log.warn('%r', e)
|
|
||||||
self._hw.setTargetRange(self.axis, self.tolerance / self._scale)
|
|
||||||
self.setFastPoll(False)
|
|
||||||
|
|
||||||
@Command()
|
|
||||||
def stop(self):
|
|
||||||
self._idle_status = IDLE, 'stopped' if self.isBusy() else ''
|
|
||||||
self._stop()
|
|
||||||
self.status = self._idle_status
|
|
||||||
|
|
||||||
@Command(IntRange())
|
|
||||||
def move(self, value):
|
|
||||||
"""relative move by number of steps"""
|
|
||||||
self._direction = 1 if value > 0 else -1
|
|
||||||
self.check_value(self.value)
|
|
||||||
self._history = None
|
|
||||||
if DIRECTION_NAME[self._direction] in self._error_state:
|
|
||||||
raise BadValueError('can not move (%s)' % self._error_state)
|
|
||||||
self._move_steps = value
|
|
||||||
self._idle_status = IDLE, ''
|
|
||||||
self.read_status()
|
|
||||||
self.setFastPoll(True, 1/self.frequency)
|
|
||||||
|
@ -1,279 +0,0 @@
|
|||||||
# *****************************************************************************
|
|
||||||
# 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 <oksana.shliakhtun@psi.ch>
|
|
||||||
# *****************************************************************************
|
|
||||||
"""Stanford Research Systems SIM900 Mainframe"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from frappy.core import StringIO, HasIO, Readable, \
|
|
||||||
Parameter, FloatRange, IntRange, EnumType, \
|
|
||||||
Property, Attached, IDLE, ERROR, WARN
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_value(value):
|
|
||||||
"""
|
|
||||||
Converting the value to float, removing the units, converting the prefix into the number.
|
|
||||||
:param value: value
|
|
||||||
:return: float value without units
|
|
||||||
"""
|
|
||||||
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
|
||||||
value, pfx = value_with_unit.match(value).groups()
|
|
||||||
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
|
||||||
if pfx in pfx_dict:
|
|
||||||
value = round(float(value) * pfx_dict[pfx], 12)
|
|
||||||
return float(value)
|
|
||||||
|
|
||||||
|
|
||||||
def find_idx(list_of_values, target):
|
|
||||||
"""
|
|
||||||
Search for the nearest value and index from the given range for the given target.
|
|
||||||
:param list_of_values: range of values
|
|
||||||
:param target: target
|
|
||||||
:return: closest index and closest value
|
|
||||||
"""
|
|
||||||
target = float(target)
|
|
||||||
cl_idx = None
|
|
||||||
cl_value = float('inf')
|
|
||||||
|
|
||||||
for idx, value in enumerate(list_of_values):
|
|
||||||
if value >= target:
|
|
||||||
diff = value - target
|
|
||||||
|
|
||||||
if diff < cl_value:
|
|
||||||
cl_value = value
|
|
||||||
cl_idx = idx
|
|
||||||
|
|
||||||
return cl_idx, cl_value
|
|
||||||
|
|
||||||
|
|
||||||
class BridgeIO(StringIO):
|
|
||||||
"""_\n is placed at the beginning of each command to distinguish
|
|
||||||
the previous response with a possible asynchronous response from the actual value returned by the method. """
|
|
||||||
|
|
||||||
end_of_line = '\n'
|
|
||||||
identification = [('_\n*IDN?', r'Stanford_Research_Systems,.*')]
|
|
||||||
|
|
||||||
|
|
||||||
class Base(HasIO):
|
|
||||||
port = Property('modules port', IntRange(0, 15))
|
|
||||||
|
|
||||||
def communicate(self, command):
|
|
||||||
"""
|
|
||||||
Connection to the particular module x.
|
|
||||||
:param command: command
|
|
||||||
:return: return
|
|
||||||
"""
|
|
||||||
return self.io.communicate(f'_\nconn {self.port:x},"_\n"\n{command}')
|
|
||||||
|
|
||||||
def query(self, command):
|
|
||||||
"""converting to float"""
|
|
||||||
return float(self.communicate(command))
|
|
||||||
|
|
||||||
|
|
||||||
class Resistance(Base, Readable):
|
|
||||||
value = Parameter('resistance', datatype=FloatRange, unit='ohm')
|
|
||||||
output_offset = Parameter('resistance deviation', datatype=FloatRange, unit='Ohm', readonly=False)
|
|
||||||
phase_hold = Parameter('phase hold', EnumType('phase hold', off=0, on=1))
|
|
||||||
|
|
||||||
RES_RANGE = ['20mOhm', '200mOhm', '2Ohm', '20Ohm', '200Ohm', '2kOhm', '20kOhm', '200kOhm',
|
|
||||||
'2MOhm', '20MOhm']
|
|
||||||
irange = Parameter('resistance range index', EnumType('resistance range index',
|
|
||||||
{name: idx for idx, name in enumerate(RES_RANGE)}),
|
|
||||||
readonly=False)
|
|
||||||
range = Parameter('resistance range value', FloatRange(2e-2, 2e7), unit='Om', readonly=False)
|
|
||||||
|
|
||||||
TIME_CONST = ['0.3s', '1s', '3s', '10s', '30s', '100s', '300s']
|
|
||||||
itc = Parameter('time constant index',
|
|
||||||
EnumType('time const. index range',
|
|
||||||
{name: value for value, name in enumerate(TIME_CONST)}), readonly=False)
|
|
||||||
tc = Parameter('time constant value', FloatRange(1e-1, 3e2), unit='s', readonly=False)
|
|
||||||
|
|
||||||
EXCT_RANGE = ['0', '3uV', '10uV', '30uV', '100uV', '300uV', '1mV', '3mV', '10mV', '30mV']
|
|
||||||
iexct = Parameter('excitation index',
|
|
||||||
EnumType('excitation index range', {name: idx for idx, name in enumerate(EXCT_RANGE, start=-1)}),
|
|
||||||
readonly=False)
|
|
||||||
exct = Parameter('excitation value', FloatRange(0, 3e-2), unit='s', default=300, readonly=False)
|
|
||||||
|
|
||||||
autorange = Parameter('autorange_on', EnumType('autorange', off=0, on=1),
|
|
||||||
readonly=False, default=0)
|
|
||||||
|
|
||||||
RES_RANGE_values = [string_to_value(value) for value in RES_RANGE]
|
|
||||||
TIME_CONST_values = [string_to_value(value) for value in TIME_CONST]
|
|
||||||
EXCT_RANGE_values = [string_to_value(value) for value in EXCT_RANGE]
|
|
||||||
|
|
||||||
ioClass = BridgeIO
|
|
||||||
|
|
||||||
def doPoll(self):
|
|
||||||
super().doPoll()
|
|
||||||
max_res = abs(self.value)
|
|
||||||
if self.autorange == 1:
|
|
||||||
if max_res >= 0.9 * self.range and self.irange < 9:
|
|
||||||
self.write_irange(self.irange + 1)
|
|
||||||
elif max_res <= 0.3 * self.range and self.irange > 0:
|
|
||||||
self.write_irange(self.irange - 1)
|
|
||||||
|
|
||||||
def read_status(self):
|
|
||||||
"""
|
|
||||||
Both the mainframe (SR900) and the module (SR921) have the same commands for the status,
|
|
||||||
here implemented commands are made for module status, not the frame!
|
|
||||||
|
|
||||||
:return: status type and message
|
|
||||||
"""
|
|
||||||
esr = int(self.communicate('*esr?')) # standart event status byte
|
|
||||||
ovsr = int(self.communicate('ovsr?')) # overload status
|
|
||||||
cesr = int(self.communicate('cesr?')) # communication error status
|
|
||||||
|
|
||||||
if esr & (1 << 1):
|
|
||||||
return ERROR, 'input error, cleared'
|
|
||||||
if esr & (1 << 2):
|
|
||||||
return ERROR, 'query error'
|
|
||||||
if esr & (1 << 4):
|
|
||||||
return ERROR, 'execution error'
|
|
||||||
if esr & (1 << 5):
|
|
||||||
return ERROR, 'command error'
|
|
||||||
if cesr & (1 << 0):
|
|
||||||
return ERROR, 'parity error'
|
|
||||||
if cesr & (1 << 2):
|
|
||||||
return ERROR, 'noise error'
|
|
||||||
if cesr & (1 << 4):
|
|
||||||
return ERROR, 'input overflow, cleared'
|
|
||||||
if cesr & (1 << 3):
|
|
||||||
return ERROR, 'hardware overflow'
|
|
||||||
if ovsr & (1 << 0):
|
|
||||||
return ERROR, 'output overload'
|
|
||||||
if cesr & (1 << 7):
|
|
||||||
return WARN, 'device clear'
|
|
||||||
if ovsr & (1 << 2):
|
|
||||||
return WARN, 'current saturation'
|
|
||||||
if ovsr & (1 << 3):
|
|
||||||
return WARN, 'under servo'
|
|
||||||
if ovsr & (1 << 4):
|
|
||||||
return WARN, 'over servo'
|
|
||||||
return IDLE, ''
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
return self.query('rval?')
|
|
||||||
|
|
||||||
def read_irange(self):
|
|
||||||
"""index of the resistance value according to the range"""
|
|
||||||
return self.query('rang?')
|
|
||||||
|
|
||||||
def write_irange(self, idx):
|
|
||||||
value = int(idx)
|
|
||||||
self.query(f'rang {value}; rang?')
|
|
||||||
self.read_range()
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_range(self):
|
|
||||||
"""value of the resistance range"""
|
|
||||||
idx = self.read_irange()
|
|
||||||
name = self.RES_RANGE[idx]
|
|
||||||
return string_to_value(name)
|
|
||||||
|
|
||||||
def write_range(self, target):
|
|
||||||
cl_idx, cl_value = find_idx(self.RES_RANGE_values, target)
|
|
||||||
self.query(f'rang {cl_idx}; rang?')
|
|
||||||
return cl_value
|
|
||||||
|
|
||||||
def read_output_offset(self):
|
|
||||||
"""Output offset, can be set by user. This is the value subtracted from the measured value"""
|
|
||||||
return self.query('rset?')
|
|
||||||
|
|
||||||
def write_output_offset(self, output_offset):
|
|
||||||
self.query(f'rset {output_offset};rset?')
|
|
||||||
|
|
||||||
def read_itc(self):
|
|
||||||
"""index of the temperature constant value according to the range"""
|
|
||||||
return self.query('tcon?')
|
|
||||||
|
|
||||||
def write_itc(self, itc):
|
|
||||||
self.read_itc()
|
|
||||||
value = int(itc)
|
|
||||||
return self.query(f'tcon {value}; tcon?')
|
|
||||||
|
|
||||||
def read_tc(self):
|
|
||||||
idx = self.read_itc()
|
|
||||||
name = self.TIME_CONST[idx]
|
|
||||||
return string_to_value(name)
|
|
||||||
|
|
||||||
def write_tc(self, target):
|
|
||||||
cl_idx, cl_value = find_idx(self.TIME_CONST_values, target)
|
|
||||||
self.query(f'tcon {cl_idx};tcon?')
|
|
||||||
return cl_value
|
|
||||||
|
|
||||||
def read_autorange(self):
|
|
||||||
return self.autorange
|
|
||||||
|
|
||||||
def write_autorange(self, value):
|
|
||||||
self.query(f'agai {value:d};agai?')
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_iexct(self):
|
|
||||||
"""index of the excitation value according to the range"""
|
|
||||||
return int(self.query('exci?'))
|
|
||||||
|
|
||||||
def write_iexct(self, iexct):
|
|
||||||
value = int(iexct)
|
|
||||||
return self.query(f'exci {value};exci?')
|
|
||||||
|
|
||||||
def write_exct(self, target):
|
|
||||||
target = float(target)
|
|
||||||
cl_idx = None
|
|
||||||
cl_value = float('inf')
|
|
||||||
min_diff = float('inf')
|
|
||||||
|
|
||||||
for idx, value in enumerate(self.EXCT_RANGE_values):
|
|
||||||
diff = abs(value - target)
|
|
||||||
|
|
||||||
if diff < min_diff:
|
|
||||||
min_diff = diff
|
|
||||||
cl_value = value
|
|
||||||
cl_idx = idx
|
|
||||||
|
|
||||||
self.write_iexct(cl_idx)
|
|
||||||
return cl_value
|
|
||||||
|
|
||||||
def read_exct(self):
|
|
||||||
idx = int(self.read_iexct())
|
|
||||||
name = self.EXCT_RANGE[idx + 1]
|
|
||||||
return string_to_value(name)
|
|
||||||
|
|
||||||
def read_phase_hold(self):
|
|
||||||
"""
|
|
||||||
Set the phase hold mode (if on - phase is assumed to be zero).
|
|
||||||
:return: 0 - off, 1 - on
|
|
||||||
"""
|
|
||||||
return int(self.communicate('phld?'))
|
|
||||||
|
|
||||||
def write_phase_hold(self, phase_hold):
|
|
||||||
self.communicate(f'phld {phase_hold}')
|
|
||||||
return self.read_phase_hold()
|
|
||||||
|
|
||||||
|
|
||||||
class Phase(Readable):
|
|
||||||
resistance = Attached()
|
|
||||||
value = Parameter('phase', FloatRange, default=0, unit='deg')
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
return self.resistance.query('phas?')
|
|
||||||
|
|
||||||
|
|
||||||
class Deviation(Readable):
|
|
||||||
resistance = Attached()
|
|
||||||
value = Parameter('resistance deviation', FloatRange(), unit='Ohm')
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
return self.resistance.query('rdev?')
|
|
576
frappy_psi/calcurve.py
Normal file
576
frappy_psi/calcurve.py
Normal file
@ -0,0 +1,576 @@
|
|||||||
|
#!/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:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
# *****************************************************************************
|
||||||
|
"""Software calibration"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from os.path import basename, dirname, exists, join
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.interpolate import PchipInterpolator, CubicSpline, PPoly # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
|
from frappy.errors import ProgrammingError, RangeError
|
||||||
|
from frappy.lib import clamp
|
||||||
|
|
||||||
|
to_scale = {
|
||||||
|
'lin': lambda x: x,
|
||||||
|
'log': lambda x: np.log10(x),
|
||||||
|
}
|
||||||
|
from_scale = {
|
||||||
|
'lin': lambda x: x,
|
||||||
|
'log': lambda x: 10 ** np.array(x),
|
||||||
|
}
|
||||||
|
TYPES = [ # lakeshore type, inp-type, loglog
|
||||||
|
('DT', 'si', False), # Si diode
|
||||||
|
('TG', 'gaalas', False), # GaAlAs diode
|
||||||
|
('PT', 'pt250', False), # platinum, 250 Ohm range
|
||||||
|
('PT', 'pt500', False), # platinum, 500 Ohm range
|
||||||
|
('PT', 'pt2500', False), # platinum, 2500 Ohm range
|
||||||
|
('RF', 'rhfe', False), # rhodium iron
|
||||||
|
('CC', 'c', True), # carbon, LakeShore acronym unknown
|
||||||
|
('CX', 'cernox', True), # Cernox
|
||||||
|
('RX', 'ruox', True), # rutheniumm oxide
|
||||||
|
('GE', 'ge', True), # germanium, LakeShore acronym unknown
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
OPTION_TYPE = {
|
||||||
|
'loglog': 0, # boolean
|
||||||
|
'extrange': 2, # tuple(min T, max T for extrapolation
|
||||||
|
'calibrange': 2, # tuple(min T, max T)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HasOptions:
|
||||||
|
def insert_option(self, key, value):
|
||||||
|
key = key.strip()
|
||||||
|
argtype = OPTION_TYPE.get(key, 1)
|
||||||
|
if argtype == 1: # one number or string
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
value = value.strip()
|
||||||
|
elif argtype == 0:
|
||||||
|
if value.strip().lower() in ('false', '0'):
|
||||||
|
value = False
|
||||||
|
else:
|
||||||
|
value = True
|
||||||
|
else:
|
||||||
|
value = [float(f) for f in value.split(',')]
|
||||||
|
self.options[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
class StdParser(HasOptions):
|
||||||
|
"""parser used for reading columns"""
|
||||||
|
def __init__(self, **options):
|
||||||
|
"""keys of options may be either 'x' or 'logx' and either 'y' or 'logy'
|
||||||
|
|
||||||
|
default is x=0, y=1
|
||||||
|
"""
|
||||||
|
if 'logx' in options:
|
||||||
|
self.xscale = 'log'
|
||||||
|
self.xcol = options.pop('logx')
|
||||||
|
else:
|
||||||
|
self.xscale = 'lin'
|
||||||
|
self.xcol = options.pop('x', 0)
|
||||||
|
if 'logy' in options:
|
||||||
|
self.yscale = 'log'
|
||||||
|
self.ycol = options.pop('logy')
|
||||||
|
else:
|
||||||
|
self.yscale = 'lin'
|
||||||
|
self.ycol = options.pop('y', 1)
|
||||||
|
self.xdata, self.ydata = [], []
|
||||||
|
self.options = options
|
||||||
|
self.invalid_lines = []
|
||||||
|
|
||||||
|
def parse(self, line):
|
||||||
|
"""get numbers from a line and put them to self.xdata / self.ydata"""
|
||||||
|
row = line.split()
|
||||||
|
try:
|
||||||
|
self.xdata.append(float(row[self.xcol]))
|
||||||
|
self.ydata.append(float(row[self.ycol]))
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
self.invalid_lines.append(line)
|
||||||
|
return
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InpParser(StdParser):
|
||||||
|
"""M. Zollikers *.inp calcurve format"""
|
||||||
|
HEADERLINE = re.compile(r'#?(?:(\w+)\s*=\s*([^!# \t\n]*)|curv.*)')
|
||||||
|
INP_TYPES = {ityp: (ltyp, loglog) for ltyp, ityp, loglog in TYPES}
|
||||||
|
|
||||||
|
def __init__(self, **options):
|
||||||
|
options.update(x=0, y=1)
|
||||||
|
super().__init__(**options)
|
||||||
|
self.header = True
|
||||||
|
|
||||||
|
def parse(self, line):
|
||||||
|
"""scan header"""
|
||||||
|
if self.header:
|
||||||
|
match = self.HEADERLINE.match(line)
|
||||||
|
if match:
|
||||||
|
key, value = match.groups()
|
||||||
|
if key is None:
|
||||||
|
self.header = False
|
||||||
|
else:
|
||||||
|
key = key.lower()
|
||||||
|
value = value.strip()
|
||||||
|
if key == 'type':
|
||||||
|
type_, loglog = self.INP_TYPES.get(value.lower(), (None, None))
|
||||||
|
if type_ is not None:
|
||||||
|
self.options['type'] = type_
|
||||||
|
if loglog is not None:
|
||||||
|
self.options['loglog'] = loglog
|
||||||
|
else:
|
||||||
|
self.insert_option(key, value)
|
||||||
|
return
|
||||||
|
elif line.startswith('!'):
|
||||||
|
return
|
||||||
|
super().parse(line)
|
||||||
|
|
||||||
|
|
||||||
|
class Parser340(StdParser):
|
||||||
|
"""parser for LakeShore *.340 files"""
|
||||||
|
HEADERLINE = re.compile(r'([^:]*):\s*([^(]*)')
|
||||||
|
CALIBHIGH = dict(L=325, M=420, H=500, B=40)
|
||||||
|
|
||||||
|
def __init__(self, **options):
|
||||||
|
options.update(x=1, y=2)
|
||||||
|
super().__init__(**options)
|
||||||
|
self.header = True
|
||||||
|
|
||||||
|
def parse(self, line):
|
||||||
|
"""scan header"""
|
||||||
|
if self.header:
|
||||||
|
match = self.HEADERLINE.match(line)
|
||||||
|
if match:
|
||||||
|
key, value = match.groups()
|
||||||
|
key = ''.join(key.split()).lower()
|
||||||
|
value = value.strip()
|
||||||
|
if key == 'dataformat':
|
||||||
|
if value[0:1] == '4':
|
||||||
|
self.xscale, self.yscale = 'log', 'lin' # logOhm
|
||||||
|
self.options['loglog'] = True
|
||||||
|
elif value[0:1] == '5':
|
||||||
|
self.xscale, self.yscale = 'log', 'log' # logOhm, logK
|
||||||
|
self.options['loglog'] = True
|
||||||
|
elif value[0:1] in ('1', '2', '3'):
|
||||||
|
self.options['loglog'] = False
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid Data Format')
|
||||||
|
self.options['scale'] = self.xscale + self.yscale
|
||||||
|
self.insert_option(key, value)
|
||||||
|
return
|
||||||
|
if 'No.' in line:
|
||||||
|
self.header = False
|
||||||
|
return
|
||||||
|
if len(line.split()) != 3:
|
||||||
|
return
|
||||||
|
super().parse(line)
|
||||||
|
if self.header and self.xdata:
|
||||||
|
# valid line detected
|
||||||
|
self.header = False
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
model = self.options.get('sensormodel', '').split('-')
|
||||||
|
if model[0]:
|
||||||
|
self.options['type'] = model[0]
|
||||||
|
if 'calibrange' not in self.options:
|
||||||
|
if len(model) > 2:
|
||||||
|
try: # e.g. model[-1] == 1.4M -> calibrange = 1.4, 420
|
||||||
|
self.options['calibrange'] = float(model[-1][:-1]), self.CALIBHIGH[model[-1][-1]]
|
||||||
|
return
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CaldatParser(StdParser):
|
||||||
|
"""parser for format from sea/tcl/startup/calib_ext.tcl"""
|
||||||
|
|
||||||
|
def __init__(self, options):
|
||||||
|
options.update(x=1, y=2)
|
||||||
|
super().__init__(options)
|
||||||
|
|
||||||
|
|
||||||
|
PARSERS = {
|
||||||
|
"340": Parser340,
|
||||||
|
"inp": InpParser,
|
||||||
|
"caldat": CaldatParser,
|
||||||
|
"dat": StdParser, # lakeshore raw data *.dat format
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check(x, y, islog):
|
||||||
|
# check interpolation error
|
||||||
|
yi = y[:-2] + (x[1:-1] - x[:-2]) * (y[2:] - y[:-2]) / (x[2:] - x[:-2])
|
||||||
|
if islog:
|
||||||
|
return sum((yi - y[1:-1]) ** 2)
|
||||||
|
return sum((np.log10(yi) - np.log10(y[1:-1])) ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_curve(newscale, curves):
|
||||||
|
"""get curve from curve cache (converts not existing ones)
|
||||||
|
|
||||||
|
:param newscale: the new scale to get
|
||||||
|
:param curves: a dict <scale> of <array> storing available scales
|
||||||
|
:return: the retrieved or converted curve
|
||||||
|
"""
|
||||||
|
if newscale in curves:
|
||||||
|
return curves[newscale]
|
||||||
|
for scale, array in curves.items():
|
||||||
|
curves[newscale] = curve = to_scale[newscale](from_scale[scale](array))
|
||||||
|
return curve
|
||||||
|
|
||||||
|
|
||||||
|
class CalCurve(HasOptions):
|
||||||
|
EXTRAPOLATION_AMOUNT = 0.1
|
||||||
|
MAX_EXTRAPOLATION_FACTOR = 2
|
||||||
|
|
||||||
|
def __init__(self, calibspec=None, *, x=None, y=None, cubic_spline=True, **options):
|
||||||
|
"""calibration curve
|
||||||
|
|
||||||
|
:param calibspec: a string with name or filename, options
|
||||||
|
lookup path for files in env. variable FRAPPY_CALIB_PATH
|
||||||
|
calibspec format:
|
||||||
|
[<full path> | <name>][,<key>=<value> ...]
|
||||||
|
for <key>/<value> as in parser arguments
|
||||||
|
:param x, y: x and y arrays (given instead of calibspec)
|
||||||
|
:param cubic_split: set to False for always using Pchip interpolation
|
||||||
|
:param options: options for parsers
|
||||||
|
"""
|
||||||
|
self.options = options
|
||||||
|
if calibspec is None:
|
||||||
|
parser = StdParser()
|
||||||
|
parser.xdata = x
|
||||||
|
parser.ydata = y
|
||||||
|
else:
|
||||||
|
if x or y:
|
||||||
|
raise ProgrammingError('can not give both calibspec and x,y ')
|
||||||
|
sensopt = calibspec.split(',')
|
||||||
|
calibname = sensopt.pop(0)
|
||||||
|
_, dot, ext = basename(calibname).rpartition('.')
|
||||||
|
kind = None
|
||||||
|
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(':')
|
||||||
|
pathlist.append(join(dirname(__file__), 'calcurves'))
|
||||||
|
for path in pathlist:
|
||||||
|
# first try without adding kind
|
||||||
|
filename = join(path.strip(), calibname)
|
||||||
|
if exists(filename):
|
||||||
|
kind = ext if dot else None
|
||||||
|
break
|
||||||
|
# then try adding all kinds as extension
|
||||||
|
for nam in calibname, calibname.upper(), calibname.lower():
|
||||||
|
for kind in PARSERS:
|
||||||
|
filename = join(path.strip(), '%s.%s' % (nam, kind))
|
||||||
|
if exists(filename):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(calibname)
|
||||||
|
sensopt = iter(sensopt)
|
||||||
|
for opt in sensopt:
|
||||||
|
key, _, value = opt.lower().partition('=')
|
||||||
|
if OPTION_TYPE.get(key) == 2:
|
||||||
|
self.options[key] = float(value), float(next(sensopt))
|
||||||
|
else:
|
||||||
|
self.insert_option(key, value)
|
||||||
|
kind = self.options.pop('kind', kind)
|
||||||
|
cls = PARSERS.get(kind, StdParser)
|
||||||
|
try:
|
||||||
|
parser = cls(**self.options)
|
||||||
|
with open(filename, encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
parser.parse(line)
|
||||||
|
parser.finish()
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError('error parsing calib curve %s %r' % (calibspec, e)) from e
|
||||||
|
# take defaults from parser options
|
||||||
|
self.options = dict(parser.options, **self.options)
|
||||||
|
|
||||||
|
x = np.asarray(parser.xdata)
|
||||||
|
y = np.asarray(parser.ydata)
|
||||||
|
if len(x) < 2:
|
||||||
|
raise ValueError('calib file %s has less than 2 points' % calibspec)
|
||||||
|
|
||||||
|
if x[0] > x[-1]:
|
||||||
|
x = np.flip(np.array(x))
|
||||||
|
y = np.flip(np.array(y))
|
||||||
|
else:
|
||||||
|
x = np.array(x)
|
||||||
|
y = np.array(y)
|
||||||
|
not_incr_idx = np.argwhere(x[1:] <= x[:-1])
|
||||||
|
if len(not_incr_idx):
|
||||||
|
raise RangeError('x not monotonic at x=%.4g' % x[not_incr_idx[0]])
|
||||||
|
|
||||||
|
self.x = {parser.xscale: x}
|
||||||
|
self.y = {parser.yscale: y}
|
||||||
|
self.lin_forced = [parser.yscale == 'lin' and (y[0] <= 0 or y[-1] <= 0),
|
||||||
|
parser.xscale == 'lin' and x[0] <= 0]
|
||||||
|
if sum(self.lin_forced):
|
||||||
|
self.loglog = False
|
||||||
|
else:
|
||||||
|
self.loglog = self.options.get('loglog', y[0] > y[-1]) # loglog defaults to True for NTC
|
||||||
|
newscale = 'log' if self.loglog else 'lin'
|
||||||
|
self.scale = newscale
|
||||||
|
x = get_curve(newscale, self.x)
|
||||||
|
y = get_curve(newscale, self.y)
|
||||||
|
self.convert_x = to_scale[newscale]
|
||||||
|
self.convert_y = from_scale[newscale]
|
||||||
|
self.calibrange = self.options.get('calibrange')
|
||||||
|
dirty = set()
|
||||||
|
self.extra_points = False
|
||||||
|
self.cutted = False
|
||||||
|
if self.calibrange:
|
||||||
|
self.calibrange = sorted(self.calibrange)
|
||||||
|
# determine indices (ibeg, iend) of first and last well calibrated point
|
||||||
|
ylin = get_curve('lin', self.y)
|
||||||
|
beg, end = self.calibrange
|
||||||
|
if y[0] > y[-1]:
|
||||||
|
ylin = -ylin
|
||||||
|
beg, end = -end, -beg
|
||||||
|
|
||||||
|
ibeg, iend = np.searchsorted(ylin, (beg, end))
|
||||||
|
if ibeg > 0 and abs(ylin[ibeg-1] - beg) < 0.1 * (ylin[ibeg] - ylin[ibeg-1]):
|
||||||
|
# add previous point, if close
|
||||||
|
ibeg -= 1
|
||||||
|
if iend < len(ylin) and abs(ylin[iend] - end) < 0.1 * (ylin[iend] - ylin[iend-1]):
|
||||||
|
# add next point, if close
|
||||||
|
iend += 1
|
||||||
|
if self.options.get('cut', False):
|
||||||
|
self.cutted = True
|
||||||
|
x = x[ibeg:iend]
|
||||||
|
y = y[ibeg:iend]
|
||||||
|
self.x = {newscale: x}
|
||||||
|
self.y = {newscale: y}
|
||||||
|
ibeg = 0
|
||||||
|
iend = len(x)
|
||||||
|
dirty.add('xy')
|
||||||
|
else:
|
||||||
|
self.extra_points = ibeg, len(x) - iend
|
||||||
|
else:
|
||||||
|
ibeg = 0
|
||||||
|
iend = len(x)
|
||||||
|
ylin = get_curve('lin', self.y)
|
||||||
|
self.calibrange = tuple(sorted([ylin[0], ylin[-1]]))
|
||||||
|
|
||||||
|
if cubic_spline:
|
||||||
|
# fit well calibrated part with spline
|
||||||
|
# determine slopes of calibrated part with CubicSpline
|
||||||
|
spline = CubicSpline(x[ibeg:iend], y[ibeg:iend])
|
||||||
|
roots = spline.derivative().roots(extrapolate=False)
|
||||||
|
if len(roots):
|
||||||
|
cubic_spline = False
|
||||||
|
|
||||||
|
self.cubic_spline = cubic_spline
|
||||||
|
if cubic_spline:
|
||||||
|
coeff = spline.c
|
||||||
|
if self.extra_points:
|
||||||
|
p = PchipInterpolator(x, y).c
|
||||||
|
# use Pchip outside left and right of calibrange
|
||||||
|
# remark: first derivative at end of calibrange is not continuous
|
||||||
|
coeff = np.concatenate((p[:, :ibeg], coeff, p[:, iend-1:]), axis=1)
|
||||||
|
else:
|
||||||
|
spline = PchipInterpolator(x, y)
|
||||||
|
coeff = spline.c
|
||||||
|
# extrapolation extension
|
||||||
|
# linear extrapolation is more robust than spline extrapolation
|
||||||
|
x1, x2 = x[0], x[-1]
|
||||||
|
# take slope at end of calibrated range for extrapolation
|
||||||
|
slopes = spline([x[ibeg], x[iend-1]], 1)
|
||||||
|
for i, j in enumerate([ibeg, iend-2]):
|
||||||
|
# slope of last interval in calibrange
|
||||||
|
si = (y[j+1] - y[j])/(x[j+1] - x[j])
|
||||||
|
# make sure slope is not more than a factor 2 different
|
||||||
|
# from the slope calculated at the outermost calibrated intervals
|
||||||
|
slopes[i] = clamp(slopes[i], 2*si, 0.5 * si)
|
||||||
|
dx = 0.1 if self.loglog else (x2 - x1) * 0.1
|
||||||
|
xe = np.concatenate(([x1 - dx], x, [x2 + dx]))
|
||||||
|
# x3 = np.append(x, x2 + dx)
|
||||||
|
# y3 = np.append(y, y[-1] + slope * dx)
|
||||||
|
y0 = y[0] - slopes[0] * dx
|
||||||
|
coeff = np.concatenate(([[0], [0], [slopes[0]], [y0]], coeff, [[0], [0], [slopes[1]], [y[-1]]]), axis=1)
|
||||||
|
self.spline = PPoly(coeff, xe)
|
||||||
|
# ranges without extrapolation:
|
||||||
|
self.xrange = get_curve('lin', self.x)[[0, -1]]
|
||||||
|
self.yrange = sorted(get_curve('lin', self.y)[[0, -1]])
|
||||||
|
self.calibrange = [max(self.calibrange[0], self.yrange[0]),
|
||||||
|
min(self.calibrange[1], self.yrange[1])]
|
||||||
|
self.set_extrapolation()
|
||||||
|
|
||||||
|
# check
|
||||||
|
# ys = self.spline(xe)
|
||||||
|
# ye = np.concatenate(([y0], y, [y[-1] + slope2 * dx]))
|
||||||
|
# assert np.all(np.abs(ys - ye) < 1e-5 * (0.1 + np.abs(ys + ye)))
|
||||||
|
|
||||||
|
def set_extrapolation(self, extleft=None, extright=None):
|
||||||
|
"""set default extrapolation range for export method
|
||||||
|
|
||||||
|
:param extleft: y value for the lower end of the extrapolation
|
||||||
|
:param extright: y value for the upper end of the extrapolation
|
||||||
|
|
||||||
|
if arguments omitted or None are replaced by a default extrapolation scheme
|
||||||
|
|
||||||
|
on return self.extx and self.exty are set to the extrapolated ranges
|
||||||
|
"""
|
||||||
|
yc1, yc2 = self.calibrange
|
||||||
|
y1, y2 = to_scale[self.scale]([yc1, yc2])
|
||||||
|
d = (y2 - y1) * self.EXTRAPOLATION_AMOUNT
|
||||||
|
yex1, yex2 = tuple(from_scale[self.scale]([y1 - d, y2 + d]))
|
||||||
|
t1, t2 = tuple(from_scale[self.scale]([y1, y2]))
|
||||||
|
|
||||||
|
# raw units, excluding extrapolation points at end
|
||||||
|
xrng = self.spline.x[1], self.spline.x[-2]
|
||||||
|
# first and last point
|
||||||
|
yp1, yp2 = sorted(from_scale[self.scale](self.spline(xrng)))
|
||||||
|
xrng = from_scale[self.scale](xrng)
|
||||||
|
|
||||||
|
# limit by maximal factor
|
||||||
|
f = self.MAX_EXTRAPOLATION_FACTOR
|
||||||
|
# but ext range should be at least to the points in curve
|
||||||
|
self.exty = [min(yp1, max(yex1, min(t1 / f, t1 * f))),
|
||||||
|
max(yp2, min(yex2, max(t2 * f, t2 / f)))]
|
||||||
|
if extleft is not None:
|
||||||
|
self.exty[0] = min(extleft, yp1)
|
||||||
|
if extright is not None:
|
||||||
|
self.exty[1] = max(extright, yp2)
|
||||||
|
self.extx = sorted(self.invert(*yd) for yd in zip(self.exty, xrng))
|
||||||
|
# check that sensor range is not extended by more than a factor f
|
||||||
|
extnew = [max(self.extx[0], min(xrng[0] / f, xrng[0] * f)),
|
||||||
|
min(self.extx[1], max(xrng[1] / f, xrng[1] * f))]
|
||||||
|
if extnew != self.extx:
|
||||||
|
# need further reduction
|
||||||
|
self.extx = extnew
|
||||||
|
self.exty = sorted(self(extnew))
|
||||||
|
|
||||||
|
def convert(self, value):
|
||||||
|
"""convert a single value
|
||||||
|
|
||||||
|
return a tuple (converted value, boolean: was it clamped?)
|
||||||
|
"""
|
||||||
|
x = clamp(value, *self.extx)
|
||||||
|
return self(x), x == value
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""convert value or numpy array without checking extrapolation range"""
|
||||||
|
return self.convert_y(self.spline(self.convert_x(value)))
|
||||||
|
|
||||||
|
def invert(self, y, defaultx=None, xscale=True, yscale=True):
|
||||||
|
"""invert y, return defaultx if no solution is found"""
|
||||||
|
if yscale:
|
||||||
|
y = to_scale[self.scale](y)
|
||||||
|
r = self.spline.solve(y)
|
||||||
|
try:
|
||||||
|
if xscale:
|
||||||
|
return from_scale[self.scale](r[0])
|
||||||
|
return r[0]
|
||||||
|
except IndexError:
|
||||||
|
return defaultx
|
||||||
|
|
||||||
|
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None):
|
||||||
|
"""export curve for downloading to hardware
|
||||||
|
|
||||||
|
:param nmax: max number of points. if the number of given points is bigger,
|
||||||
|
the points with the lowest interpolation error are omitted
|
||||||
|
:param logformat: a list with two elements of None, True or False
|
||||||
|
True: use log, False: use line, None: use log if self.loglog
|
||||||
|
values None are replaced with the effectively used format
|
||||||
|
False / True are replaced by [False, False] / [True, True]
|
||||||
|
default is False
|
||||||
|
:param yrange: to reduce or extrapolate to this interval (extrapolate is ignored when given)
|
||||||
|
:param extrapolate: a flag indicating whether the curves should be extrapolated
|
||||||
|
to the preset extrapolation range
|
||||||
|
:param xlimits: max x range
|
||||||
|
:return: numpy array with 2 dimensions returning the curve
|
||||||
|
"""
|
||||||
|
|
||||||
|
if logformat in (True, False):
|
||||||
|
logformat = [logformat, logformat]
|
||||||
|
try:
|
||||||
|
scales = []
|
||||||
|
for idx, logfmt in enumerate(logformat):
|
||||||
|
if logfmt and self.lin_forced[idx]:
|
||||||
|
raise ValueError('%s must contain positive values only' % 'xy'[idx])
|
||||||
|
logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
|
||||||
|
scales.append('log' if linlog else 'lin')
|
||||||
|
xscale, yscale = scales
|
||||||
|
except (TypeError, AssertionError):
|
||||||
|
raise ValueError('logformat must be a 2 element list or a boolean')
|
||||||
|
|
||||||
|
x = self.spline.x[1:-1] # raw units, excluding extrapolated points
|
||||||
|
x1, x2 = xmin, xmax = x[0], x[-1]
|
||||||
|
y1, y2 = sorted(self.spline([x1, x2]))
|
||||||
|
|
||||||
|
if extrapolate and not yrange:
|
||||||
|
yrange = self.exty
|
||||||
|
if yrange is not None:
|
||||||
|
xmin, xmax = sorted(self.invert(*yd, xscale=False) for yd in zip(yrange, [x1, x2]))
|
||||||
|
if xlimits is not None:
|
||||||
|
lim = to_scale[self.scale](xlimits)
|
||||||
|
xmin = clamp(xmin, *lim)
|
||||||
|
xmax = clamp(xmax, *lim)
|
||||||
|
if xmin != x1 or xmax != x2:
|
||||||
|
ibeg, iend = np.searchsorted(x, (xmin, xmax))
|
||||||
|
if abs(x[ibeg] - xmin) < 0.1 * (x[ibeg + 1] - x[ibeg]):
|
||||||
|
# remove first point, if close
|
||||||
|
ibeg += 1
|
||||||
|
if abs(x[iend - 1] - xmax) < 0.1 * (x[iend - 1] - x[iend - 2]):
|
||||||
|
# remove last point, if close
|
||||||
|
iend -= 1
|
||||||
|
x = np.concatenate(([xmin], x[ibeg:iend], [xmax]))
|
||||||
|
y = self.spline(x)
|
||||||
|
|
||||||
|
# convert to exported scale
|
||||||
|
if xscale != self.scale:
|
||||||
|
x = to_scale[xscale](from_scale[self.scale](x))
|
||||||
|
if yscale != self.scale:
|
||||||
|
y = to_scale[yscale](from_scale[self.scale](y))
|
||||||
|
|
||||||
|
# reduce number of points, if needed
|
||||||
|
n = len(x)
|
||||||
|
i, j = 1, n - 1 # index range for calculating interpolation deviation
|
||||||
|
deviation = np.zeros(n)
|
||||||
|
while True:
|
||||||
|
# calculate interpolation error when a single point is omitted
|
||||||
|
ym = y[i-1:j-1] + (x[i:j] - x[i-1:j-1]) * (y[i+1:j+1] - y[i-1:j-1]) / (x[i+1:j+1] - x[i-1:j-1])
|
||||||
|
if yscale == 'log':
|
||||||
|
deviation[i:j] = np.abs(ym - y[i:j])
|
||||||
|
else:
|
||||||
|
deviation[i:j] = np.abs(ym - y[i:j]) / (np.abs(ym + y[i:j]) + 1e-10)
|
||||||
|
if n <= nmax:
|
||||||
|
break
|
||||||
|
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
|
||||||
|
y = np.delete(y, idx)
|
||||||
|
x = np.delete(x, idx)
|
||||||
|
deviation = np.delete(deviation, idx)
|
||||||
|
n -= 1
|
||||||
|
# index range to recalculate
|
||||||
|
i, j = max(1, idx - 1), min(n - 1, idx + 1)
|
||||||
|
self.deviation = deviation # for debugging purposes
|
||||||
|
return np.stack([x, y], axis=1)
|
313
frappy_psi/cetoni_pump.py
Normal file
313
frappy_psi/cetoni_pump.py
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
libpath = '/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/python/src/'
|
||||||
|
import sys
|
||||||
|
if libpath not in sys.path:
|
||||||
|
sys.path.append(libpath)
|
||||||
|
|
||||||
|
from frappy.core import Drivable, Readable, StringIO, HasIO, FloatRange, IntRange, StringType, BoolType, EnumType, \
|
||||||
|
Parameter, Property, PersistentParam, Command, IDLE, BUSY, ERROR, WARN, Attached, Module
|
||||||
|
from qmixsdk import qmixbus
|
||||||
|
from qmixsdk import qmixpump
|
||||||
|
from qmixsdk import qmixvalve
|
||||||
|
from qmixsdk.qmixpump import ContiFlowProperty, ContiFlowSwitchingMode
|
||||||
|
from qmixsdk.qmixbus import UnitPrefix, TimeUnit
|
||||||
|
import time
|
||||||
|
|
||||||
|
class LabCannBus(Module):
|
||||||
|
deviceconfig = Property('config files', StringType(),default="/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/dual_pumps")
|
||||||
|
|
||||||
|
def earlyInit(self):
|
||||||
|
super().earlyInit()
|
||||||
|
self.bus = qmixbus.Bus()
|
||||||
|
self.bus.open(self.deviceconfig, "")
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
self.bus.start()
|
||||||
|
|
||||||
|
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
|
||||||
|
f.write('200 50 3')
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
"""Close the connection"""
|
||||||
|
self.bus.stop()
|
||||||
|
self.bus.close()
|
||||||
|
|
||||||
|
|
||||||
|
class SyringePump(Drivable):
|
||||||
|
io = Attached()
|
||||||
|
pump_name = Property('name of pump', StringType(),default="Nemesys_S_1_Pump")
|
||||||
|
valve_name = Property('name of valve', StringType(),default="Nemesys_S_1_Valve")
|
||||||
|
|
||||||
|
inner_diameter_set = Property('inner diameter', FloatRange(), default=1)
|
||||||
|
piston_stroke_set = Property('piston stroke', FloatRange(), default=60)
|
||||||
|
|
||||||
|
value = Parameter('volume', FloatRange(unit='uL'))
|
||||||
|
status = Parameter()
|
||||||
|
|
||||||
|
max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='uL/s',), readonly=True)
|
||||||
|
max_volume = Parameter('max volume', FloatRange(0,100000, unit='uL',), readonly=True)
|
||||||
|
|
||||||
|
target_flow_rate = Parameter('target flow rate', FloatRange(unit='uL/s'), readonly=False)
|
||||||
|
real_flow_rate = Parameter('actual flow rate', FloatRange(unit='uL/s'), readonly=True)
|
||||||
|
|
||||||
|
target = Parameter('target volume', FloatRange(unit='uL'), readonly=False)
|
||||||
|
|
||||||
|
no_of_valve_pos = Property('number of valve positions', IntRange(0,10), default=1)
|
||||||
|
valve_pos = Parameter('valve position', EnumType('valve', CLOSED=0, APP=1, RES=2, OPEN=3), readonly=False)
|
||||||
|
|
||||||
|
force = Parameter('syringe force', FloatRange(unit='kN'), readonly=True)
|
||||||
|
max_force = Parameter('max device force', FloatRange(unit='kN'), readonly=True)
|
||||||
|
force_limit = Parameter('user force limit', FloatRange(unit='kN'), readonly=False)
|
||||||
|
|
||||||
|
_resolving_force_overload = False
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
|
||||||
|
self.pump = qmixpump.Pump()
|
||||||
|
self.pump.lookup_by_name(self.pump_name)
|
||||||
|
|
||||||
|
self.valve = qmixvalve.Valve()
|
||||||
|
self.valve.lookup_by_name(self.valve_name)
|
||||||
|
|
||||||
|
|
||||||
|
def initialReads(self):
|
||||||
|
if self.pump.is_in_fault_state():
|
||||||
|
self.pump.clear_fault()
|
||||||
|
|
||||||
|
if not self.pump.is_enabled():
|
||||||
|
self.pump.enable(True)
|
||||||
|
|
||||||
|
self.pump.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
|
||||||
|
|
||||||
|
self.pump.set_volume_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres)
|
||||||
|
|
||||||
|
self.pump.set_flow_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_second)
|
||||||
|
|
||||||
|
self.max_flow_rate = round(self.pump.get_flow_rate_max(),2)
|
||||||
|
self.max_volume = round(self.pump.get_volume_max(),2)
|
||||||
|
self.valve_pos = self.valve.actual_valve_position()
|
||||||
|
|
||||||
|
self.target_flow_rate = round(self.max_flow_rate * 0.5,2)
|
||||||
|
self.target = max(0, round(self.pump.get_fill_level(),2))
|
||||||
|
|
||||||
|
self.pump.enable_force_monitoring(True)
|
||||||
|
self.max_force = self.pump.get_max_device_force()
|
||||||
|
self.force_limit = self.max_force
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return round(self.pump.get_fill_level(),2)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if self.read_valve_pos() == 0 :
|
||||||
|
self.status = ERROR, 'Cannot pump if valve is closed'
|
||||||
|
self.log.warn('Cannot pump if valve is closed')
|
||||||
|
return target
|
||||||
|
else:
|
||||||
|
self.pump.set_fill_level(target, self.target_flow_rate)
|
||||||
|
self.status = BUSY, 'Target changed'
|
||||||
|
self.log.info(f'Started pumping at {self.target_flow_rate} ul/s')
|
||||||
|
return target
|
||||||
|
|
||||||
|
def write_target_flow_rate(self, rate):
|
||||||
|
self.target_flow_rate = rate
|
||||||
|
return rate
|
||||||
|
|
||||||
|
def read_real_flow_rate(self):
|
||||||
|
return round(self.pump.get_flow_is(),2)
|
||||||
|
|
||||||
|
def read_valve_pos(self):
|
||||||
|
return self.valve.actual_valve_position()
|
||||||
|
|
||||||
|
def write_valve_pos(self, target_pos):
|
||||||
|
self.valve.switch_valve_to_position(target_pos)
|
||||||
|
return target_pos
|
||||||
|
|
||||||
|
def read_force(self):
|
||||||
|
return round(self.pump.read_force_sensor(),3)
|
||||||
|
|
||||||
|
def read_force_limit(self):
|
||||||
|
return self.pump.get_force_limit()
|
||||||
|
|
||||||
|
def write_force_limit(self, limit):
|
||||||
|
self.pump.write_force_limit(limit)
|
||||||
|
return limit
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
fault_state = self.pump.is_in_fault_state()
|
||||||
|
pumping = self.pump.is_pumping()
|
||||||
|
pump_enabled = self.pump.is_enabled()
|
||||||
|
safety_stop_active = self.pump.is_force_safety_stop_active()
|
||||||
|
|
||||||
|
if fault_state == True:
|
||||||
|
return ERROR, 'Pump in fault state'
|
||||||
|
elif self._resolving_force_overload :
|
||||||
|
return BUSY, 'Resolving force overload'
|
||||||
|
elif safety_stop_active:
|
||||||
|
return ERROR, 'Pressure safety stop'
|
||||||
|
elif not pump_enabled:
|
||||||
|
return ERROR, 'Pump not enabled'
|
||||||
|
elif pumping == True:
|
||||||
|
return BUSY, f'Pumping {self.real_flow_rate} ul/s'
|
||||||
|
elif self.read_valve_pos() == 0:
|
||||||
|
return IDLE, 'Valve closed'
|
||||||
|
else:
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def stop(self):
|
||||||
|
self.pump.stop_pumping()
|
||||||
|
self.target = self.pump.get_fill_level()
|
||||||
|
self.status = BUSY, 'Stopping'
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def clear_errors(self):
|
||||||
|
"""Clear fault state and enable pump"""
|
||||||
|
if self.pump.is_in_fault_state():
|
||||||
|
self.pump.clear_fault()
|
||||||
|
self.log.info('Cleared faults')
|
||||||
|
|
||||||
|
if not self.pump.is_enabled():
|
||||||
|
self.pump.enable(True)
|
||||||
|
self.log.info('Pump was disabled, re-enabling')
|
||||||
|
|
||||||
|
self.target = max(0,round(self.value,2))
|
||||||
|
self.status = IDLE, ''
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def resolve_force_overload(self):
|
||||||
|
"""Resolve a force overload situation"""
|
||||||
|
if not self.pump.is_force_safety_stop_active():
|
||||||
|
self.status = ERROR, 'No force overload detected'
|
||||||
|
self.log.warn('No force overload to be resolved')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._resolving_force_overload = True
|
||||||
|
self.status = BUSY, 'Resolving force overload'
|
||||||
|
self.pump.enable_force_monitoring(False)
|
||||||
|
|
||||||
|
flow = 0 - self.pump.get_flow_rate_max() / 100
|
||||||
|
self.pump.generate_flow(flow)
|
||||||
|
|
||||||
|
safety_stop_active = False
|
||||||
|
while not safety_stop_active:
|
||||||
|
time.sleep(0.1)
|
||||||
|
safety_stop_active = self.pump.is_force_safety_stop_active()
|
||||||
|
self.pump.stop_pumping()
|
||||||
|
self.pump.enable_force_monitoring(True)
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
self._resolving_force_overload = False
|
||||||
|
self.status = self.read_status()
|
||||||
|
|
||||||
|
class ContiFlowPump(Drivable):
|
||||||
|
io = Attached()
|
||||||
|
|
||||||
|
inner_diameter_set = Property('inner diameter', FloatRange(), default=1)
|
||||||
|
piston_stroke_set = Property('piston stroke', FloatRange(), default=60)
|
||||||
|
|
||||||
|
crossflow_seconds = Property('crossflow duration', FloatRange(unit='s'),default=2)
|
||||||
|
|
||||||
|
value = PersistentParam('flow rate', FloatRange(unit='uL/s'))
|
||||||
|
status = PersistentParam()
|
||||||
|
|
||||||
|
max_refill_flow = Parameter('max refill flow', FloatRange(unit='uL/s'), readonly=True)
|
||||||
|
refill_flow = Parameter('refill flow', FloatRange(unit='uL/s'), readonly=False)
|
||||||
|
|
||||||
|
max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='uL/s',), readonly=True)
|
||||||
|
target = Parameter('target flow rate', FloatRange(unit='uL/s'), readonly=False)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
|
||||||
|
self.pump = qmixpump.ContiFlowPump()
|
||||||
|
self.pump.lookup_by_name("ContiFlowPump_1")
|
||||||
|
|
||||||
|
def initialReads(self):
|
||||||
|
if self.pump.is_in_fault_state():
|
||||||
|
self.pump.clear_fault()
|
||||||
|
|
||||||
|
if not self.pump.is_enabled():
|
||||||
|
self.pump.enable(True)
|
||||||
|
|
||||||
|
self.syringe_pump1 = self.pump.get_syringe_pump(0)
|
||||||
|
self.syringe_pump1.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
|
||||||
|
self.syringe_pump2 = self.pump.get_syringe_pump(1)
|
||||||
|
self.syringe_pump2.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
|
||||||
|
self.pump.set_volume_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres)
|
||||||
|
self.pump.set_flow_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_second)
|
||||||
|
|
||||||
|
self.pump.set_device_property(ContiFlowProperty.SWITCHING_MODE, ContiFlowSwitchingMode.CROSS_FLOW)
|
||||||
|
self.max_refill_flow = self.pump.get_device_property(ContiFlowProperty.MAX_REFILL_FLOW)
|
||||||
|
self.pump.set_device_property(ContiFlowProperty.REFILL_FLOW, self.max_refill_flow / 2.0)
|
||||||
|
self.pump.set_device_property(ContiFlowProperty.CROSSFLOW_DURATION_S, self.crossflow_seconds)
|
||||||
|
self.pump.set_device_property(ContiFlowProperty.OVERLAP_DURATION_S, 0)
|
||||||
|
|
||||||
|
self.max_flow_rate = self.pump.get_flow_rate_max()
|
||||||
|
self.target = 0
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return round(self.pump.get_flow_is(),3)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if target <= 0:
|
||||||
|
self.pump.stop_pumping()
|
||||||
|
self.status = self.read_status()
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
self.pump.generate_flow(target)
|
||||||
|
self.status = BUSY, 'Target changed'
|
||||||
|
return target
|
||||||
|
|
||||||
|
def read_refill_flow(self):
|
||||||
|
return round(self.pump.get_device_property(ContiFlowProperty.REFILL_FLOW),3)
|
||||||
|
|
||||||
|
def write_refill_flow(self, refill_flow):
|
||||||
|
self.pump.set_device_property(ContiFlowProperty.REFILL_FLOW, refill_flow)
|
||||||
|
self.max_flow_rate = self.pump.get_flow_rate_max()
|
||||||
|
return refill_flow
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
fault_state = self.pump.is_in_fault_state()
|
||||||
|
pumping = self.pump.is_pumping()
|
||||||
|
pump_enabled = self.pump.is_enabled()
|
||||||
|
pump_initialised = self.pump.is_initialized()
|
||||||
|
pump_initialising = self.pump.is_initializing()
|
||||||
|
|
||||||
|
if fault_state == True:
|
||||||
|
return ERROR, 'Pump in fault state'
|
||||||
|
elif not pump_enabled:
|
||||||
|
return ERROR, 'Pump not enabled'
|
||||||
|
elif not pump_initialised:
|
||||||
|
return WARN, 'Pump not initialised'
|
||||||
|
elif pump_initialising:
|
||||||
|
return BUSY, 'Pump initialising'
|
||||||
|
elif pumping == True:
|
||||||
|
return BUSY, 'Pumping'
|
||||||
|
else:
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def stop(self):
|
||||||
|
self.pump.stop_pumping()
|
||||||
|
self.target = 0
|
||||||
|
self.status = BUSY, 'Stopping'
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def clear_errors(self):
|
||||||
|
"""Clear fault state and enable pump"""
|
||||||
|
if self.pump.is_in_fault_state():
|
||||||
|
self.pump.clear_fault()
|
||||||
|
self.log.info('Cleared faults')
|
||||||
|
|
||||||
|
if not self.pump.is_enabled():
|
||||||
|
self.pump.enable(True)
|
||||||
|
self.log.info('Pump was disabled, re-enabling')
|
||||||
|
|
||||||
|
self.target = 0
|
||||||
|
self.status = IDLE, ''
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def initialise(self):
|
||||||
|
"""Initialise the ConfiFlow pump"""
|
||||||
|
self.pump.initialize()
|
123
frappy_psi/frozenparam.py
Normal file
123
frappy_psi/frozenparam.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
# 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:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
from frappy.core import Parameter, FloatRange, IntRange, Property
|
||||||
|
from frappy.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
class FrozenParam(Parameter):
|
||||||
|
"""workaround for lazy hardware
|
||||||
|
|
||||||
|
Some hardware does not react nicely: when a parameter is changed,
|
||||||
|
and read back immediately, still the old value is returned.
|
||||||
|
This special parameter helps fixing this problem.
|
||||||
|
|
||||||
|
Mechanism:
|
||||||
|
|
||||||
|
- after a call to write_<param> for a short time (<n_polls> * <interval>)
|
||||||
|
the hardware is polled until the readback changes before the 'changed'
|
||||||
|
message is replied to the client
|
||||||
|
- if there is no change yet within short time, the 'changed' message is
|
||||||
|
set with the given value and further calls to read_<param> return also
|
||||||
|
this given value until the readback value has changed or until
|
||||||
|
<timeout> sec have passed.
|
||||||
|
|
||||||
|
For float parameters, the behaviour for small changes is improved
|
||||||
|
when the write_<param> method tries to return the (may be rounded) value,
|
||||||
|
as if it would be returned by the hardware. If this behaviour is not
|
||||||
|
known, or the programmer is too lazy to implement it, write_<param>
|
||||||
|
should return None or the given value.
|
||||||
|
Also it will help to adjust the datatype properties
|
||||||
|
'absolute_resolution' and 'relative_resolution' to reasonable values.
|
||||||
|
"""
|
||||||
|
timeout = Property('timeout for freezing readback value',
|
||||||
|
FloatRange(0, unit='s'), default=30)
|
||||||
|
n_polls = Property("""number polls within write method""",
|
||||||
|
IntRange(0), default=1)
|
||||||
|
interval = Property("""interval for polls within write method
|
||||||
|
|
||||||
|
the product n_polls * interval should not be more than a fraction of a second
|
||||||
|
in order not to block the connection for too long
|
||||||
|
""",
|
||||||
|
FloatRange(0, unit='s'), default=0.05)
|
||||||
|
new_value = None
|
||||||
|
previous_value = None
|
||||||
|
expire = 0
|
||||||
|
is_float = True # assume float. will be fixed later
|
||||||
|
|
||||||
|
def isclose(self, v1, v2):
|
||||||
|
if v1 == v2:
|
||||||
|
return True
|
||||||
|
if self.is_float:
|
||||||
|
dt = self.datatype
|
||||||
|
try:
|
||||||
|
return math.isclose(v1, v2, abs_tol=dt.absolute_tolerance,
|
||||||
|
rel_tol=dt.relative_tolerance)
|
||||||
|
except AttributeError:
|
||||||
|
# fix once for ever when datatype is not a float
|
||||||
|
self.is_float = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
try:
|
||||||
|
rfunc = getattr(owner, f'read_{name}')
|
||||||
|
wfunc = getattr(owner, f'write_{name}')
|
||||||
|
except AttributeError:
|
||||||
|
raise ProgrammingError(f'FrozenParam: methods read_{name} and write_{name} must exist') from None
|
||||||
|
|
||||||
|
super().__set_name__(owner, name)
|
||||||
|
|
||||||
|
def read_wrapper(self, pname=name, rfunc=rfunc):
|
||||||
|
pobj = self.parameters[pname]
|
||||||
|
value = rfunc(self)
|
||||||
|
if pobj.new_value is None:
|
||||||
|
return value
|
||||||
|
if not pobj.isclose(value, pobj.new_value):
|
||||||
|
if value == pobj.previous_value:
|
||||||
|
if time.time() < pobj.expire:
|
||||||
|
return pobj.new_value
|
||||||
|
self.log.warning('%s readback did not change within %g sec',
|
||||||
|
pname, pobj.timeout)
|
||||||
|
else:
|
||||||
|
# value has changed, but is not matching new value
|
||||||
|
self.log.warning('%s readback changed from %r to %r but %r was given',
|
||||||
|
pname, pobj.previous_value, value, pobj.new_value)
|
||||||
|
# readback value has changed or returned value is roughly equal to the new value
|
||||||
|
pobj.new_value = None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def write_wrapper(self, value, wfunc=wfunc, rfunc=rfunc, read_wrapper=read_wrapper, pname=name):
|
||||||
|
pobj = self.parameters[pname]
|
||||||
|
pobj.previous_value = rfunc(self)
|
||||||
|
pobj.new_value = wfunc(self, value)
|
||||||
|
if pobj.new_value is None: # as wfunc is the unwrapped write_* method, the return value may be None
|
||||||
|
pobj.new_value = value
|
||||||
|
pobj.expire = time.time() + pobj.timeout
|
||||||
|
for cnt in range(pobj.n_polls):
|
||||||
|
if cnt: # we may be lucky, and the readback value has already changed
|
||||||
|
time.sleep(pobj.interval)
|
||||||
|
value = read_wrapper(self)
|
||||||
|
if pobj.new_value is None:
|
||||||
|
return value
|
||||||
|
return pobj.new_value
|
||||||
|
|
||||||
|
setattr(owner, f'read_{name}', read_wrapper)
|
||||||
|
setattr(owner, f'write_{name}', write_wrapper)
|
104
frappy_psi/gilsonpump.py
Normal file
104
frappy_psi/gilsonpump.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Author: Wouter Gruenewald<wouter.gruenewald@psi.ch>
|
||||||
|
|
||||||
|
from frappy.core import StringType, BoolType, EnumType, FloatRange, Parameter, Property, PersistentParam, Command, IDLE, ERROR, WARN, BUSY, Drivable
|
||||||
|
|
||||||
|
|
||||||
|
class PeristalticPump(Drivable):
|
||||||
|
value = Parameter('Pump speed', FloatRange(0,100,unit="%"), default=0)
|
||||||
|
target = Parameter('Target pump speed', FloatRange(0,100,unit="%"), default=0)
|
||||||
|
status = Parameter()
|
||||||
|
|
||||||
|
addr_AO = Property('Address of the analog out', StringType())
|
||||||
|
addr_dir_relay = Property('Address of the direction relay', StringType())
|
||||||
|
addr_run_relay = Property('Address of the running relay', StringType())
|
||||||
|
|
||||||
|
direction = Parameter('pump direction', EnumType('direction', CLOCKWISE=0, ANTICLOCKWISE=1), default=0, readonly=False)
|
||||||
|
active = Parameter('pump running', BoolType(), default=False, readonly=False)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
|
||||||
|
f.write('0')
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_mode', 'w') as f :
|
||||||
|
f.write('V')
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
|
||||||
|
f.write('0')
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
|
||||||
|
f.write('1')
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
'''Disable analog output'''
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
|
||||||
|
f.write('0')
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
|
||||||
|
f.write('0')
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'r') as f :
|
||||||
|
raw_value = f.read().strip('\n')
|
||||||
|
value = (int(raw_value) / 5000) * 100
|
||||||
|
return value
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
raw_value = (target / 100)*5000
|
||||||
|
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
|
||||||
|
f.write(str(int(raw_value)))
|
||||||
|
return target
|
||||||
|
|
||||||
|
def read_direction(self):
|
||||||
|
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'r') as f :
|
||||||
|
raw_direction = f.read().strip('\n')
|
||||||
|
if raw_direction == '0' or raw_direction == 'F':
|
||||||
|
return 0
|
||||||
|
if raw_direction == '1' or raw_direction == 'S':
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def write_direction(self, direction):
|
||||||
|
if direction == 0:
|
||||||
|
raw_direction = '0'
|
||||||
|
elif direction == 1:
|
||||||
|
raw_direction = '1'
|
||||||
|
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'w') as f :
|
||||||
|
f.write(raw_direction)
|
||||||
|
return direction
|
||||||
|
|
||||||
|
def read_active(self):
|
||||||
|
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'r') as f :
|
||||||
|
raw_active = f.read().strip('\n')
|
||||||
|
if raw_active == '0' or raw_active == 'F':
|
||||||
|
return False
|
||||||
|
elif raw_active == '1' or raw_active == 'S':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def write_active(self, active):
|
||||||
|
if active == False:
|
||||||
|
raw_active = '0'
|
||||||
|
elif active == True:
|
||||||
|
raw_active = '1'
|
||||||
|
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'w') as f :
|
||||||
|
f.write(raw_active)
|
||||||
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'r') as f :
|
||||||
|
raw_direction = f.read().strip('\n')
|
||||||
|
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'r') as f :
|
||||||
|
raw_active = f.read().strip('\n')
|
||||||
|
|
||||||
|
if raw_direction == 'F' or raw_direction == 'S':
|
||||||
|
return ERROR, 'Fault on direction relay'
|
||||||
|
elif raw_active == 'F' or raw_active == 'S':
|
||||||
|
return ERROR, 'Fault on pump activation relay'
|
||||||
|
elif self.active == True:
|
||||||
|
return BUSY, 'Pump running'
|
||||||
|
else:
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def stop(self):
|
||||||
|
self.write_active(False)
|
@ -15,8 +15,6 @@
|
|||||||
#
|
#
|
||||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Thermo Haake Phoenix P1 Bath Circulator"""
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from frappy.core import StringIO, HasIO, Parameter, FloatRange, BoolType, \
|
from frappy.core import StringIO, HasIO, Parameter, FloatRange, BoolType, \
|
||||||
@ -24,13 +22,7 @@ from frappy.core import StringIO, HasIO, Parameter, FloatRange, BoolType, \
|
|||||||
from frappy_psi.convergence import HasConvergence
|
from frappy_psi.convergence import HasConvergence
|
||||||
from frappy.errors import CommunicationFailedError
|
from frappy.errors import CommunicationFailedError
|
||||||
|
|
||||||
|
|
||||||
def convert(string):
|
def convert(string):
|
||||||
"""
|
|
||||||
Converts reply to a number
|
|
||||||
:param string: reply from the command
|
|
||||||
:return: number
|
|
||||||
"""
|
|
||||||
number = re.sub(r'[^0-9.-]', '', string)
|
number = re.sub(r'[^0-9.-]', '', string)
|
||||||
return float(number)
|
return float(number)
|
||||||
|
|
||||||
@ -63,21 +55,11 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_values_status(self):
|
def get_values_status(self):
|
||||||
"""
|
|
||||||
Supplementary command for the operating status method.
|
|
||||||
Removes the extra symbol and converts each status value into integer.
|
|
||||||
|
|
||||||
:return: array of integers
|
|
||||||
"""
|
|
||||||
reply = self.communicate('B')
|
reply = self.communicate('B')
|
||||||
string = reply.rstrip('$')
|
string = reply.rstrip('$')
|
||||||
return [int(val) for val in string]
|
return [int(val) for val in string]
|
||||||
|
|
||||||
def read_status(self): # control_active update
|
def read_status(self): # control_active update
|
||||||
"""
|
|
||||||
Operating status.
|
|
||||||
:return: statu type and message
|
|
||||||
"""
|
|
||||||
values_str = self.get_values_status()
|
values_str = self.get_values_status()
|
||||||
self.read_control_active()
|
self.read_control_active()
|
||||||
|
|
||||||
@ -90,10 +72,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
|||||||
return IDLE, ''
|
return IDLE, ''
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
"""
|
|
||||||
F1 - internal temperature, F2 - external temperature
|
|
||||||
:return: float temperature value
|
|
||||||
"""
|
|
||||||
if self.mode == 1:
|
if self.mode == 1:
|
||||||
value = self.communicate('F1')
|
value = self.communicate('F1')
|
||||||
else:
|
else:
|
||||||
@ -101,11 +79,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
|||||||
return convert(value)
|
return convert(value)
|
||||||
|
|
||||||
def write_control_active(self, value):
|
def write_control_active(self, value):
|
||||||
"""
|
|
||||||
Turning on/off the heating, pump and regulation
|
|
||||||
:param value: 0 is OFF, 1 is ON
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if value is True:
|
if value is True:
|
||||||
self.communicate('GO') # heating and pump run
|
self.communicate('GO') # heating and pump run
|
||||||
self.communicate('W SR') # regulation
|
self.communicate('W SR') # regulation
|
||||||
@ -126,11 +99,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
|||||||
return convert(string)
|
return convert(string)
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
"""
|
|
||||||
Selecting Celsius, setting the target
|
|
||||||
:param target: target
|
|
||||||
:return: target
|
|
||||||
"""
|
|
||||||
self.write_control_active(True)
|
self.write_control_active(True)
|
||||||
self.read_status()
|
self.read_status()
|
||||||
self.communicate('W TE C')
|
self.communicate('W TE C')
|
||||||
@ -138,11 +106,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
|||||||
return target
|
return target
|
||||||
|
|
||||||
def write_mode(self, mode):
|
def write_mode(self, mode):
|
||||||
"""
|
|
||||||
Switching to internal or external control
|
|
||||||
:param mode: internal/external
|
|
||||||
:return: selected mode
|
|
||||||
"""
|
|
||||||
if mode == 1:
|
if mode == 1:
|
||||||
self.communicate('W IN')
|
self.communicate('W IN')
|
||||||
self.communicate('W EX')
|
self.communicate('W EX')
|
||||||
@ -150,7 +113,7 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
|||||||
|
|
||||||
@Command
|
@Command
|
||||||
def clear_errors(self):
|
def clear_errors(self):
|
||||||
""" Reset after error. Otherwise the status will not be updated"""
|
""" Reset after error"""
|
||||||
if self.read_status()[0] == ERROR:
|
if self.read_status()[0] == ERROR:
|
||||||
try:
|
try:
|
||||||
self.communicate('ER')
|
self.communicate('ER')
|
||||||
|
@ -231,14 +231,17 @@ class ResChannel(Channel):
|
|||||||
def _read_value(self):
|
def _read_value(self):
|
||||||
"""read value, without update"""
|
"""read value, without update"""
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if now + 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause:
|
if now - 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause:
|
||||||
return None
|
return None
|
||||||
result = float(self.communicate('RDGR?%d' % self.channel))
|
result = float(self.communicate('RDGR?%d' % self.channel))
|
||||||
if result == 0:
|
if result == 0:
|
||||||
|
if self.autorange:
|
||||||
|
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
||||||
|
self.write_range(min(self.MAX_RNG, rng + 1))
|
||||||
return None
|
return None
|
||||||
if self.autorange:
|
if self.autorange:
|
||||||
self.fix_autorange()
|
self.fix_autorange()
|
||||||
if now + 0.5 > self._last_range_change + self.pause:
|
if now - 0.5 > self._last_range_change + self.pause:
|
||||||
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
||||||
if self.status[0] < self.Status.ERROR:
|
if self.status[0] < self.Status.ERROR:
|
||||||
if abs(result) > self.RES_SCALE[rng]:
|
if abs(result) > self.RES_SCALE[rng]:
|
||||||
@ -251,8 +254,10 @@ class ResChannel(Channel):
|
|||||||
lim -= 0.05 # not more than 4 steps at once
|
lim -= 0.05 # not more than 4 steps at once
|
||||||
# effectively: <0.16 %: 4 steps, <1%: 3 steps, <5%: 2 steps, <20%: 1 step
|
# effectively: <0.16 %: 4 steps, <1%: 3 steps, <5%: 2 steps, <20%: 1 step
|
||||||
elif rng < self.MAX_RNG:
|
elif rng < self.MAX_RNG:
|
||||||
|
self.log.debug('increase range due to error %d', rng)
|
||||||
rng = min(self.MAX_RNG, rng + 1)
|
rng = min(self.MAX_RNG, rng + 1)
|
||||||
if rng != self.range:
|
if rng != self.range:
|
||||||
|
self.log.debug('range change to %d', rng)
|
||||||
self.write_range(rng)
|
self.write_range(rng)
|
||||||
self._last_range_change = now
|
self._last_range_change = now
|
||||||
return result
|
return result
|
||||||
@ -381,6 +386,10 @@ class TemperatureLoop(HasConvergence, TemperatureChannel, Drivable):
|
|||||||
htrrng = Parameter('', EnumType(HTRRNG), readonly=False)
|
htrrng = Parameter('', EnumType(HTRRNG), readonly=False)
|
||||||
_control_active = False
|
_control_active = False
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
super().doPoll()
|
||||||
|
self.set_htrrng()
|
||||||
|
|
||||||
@Command
|
@Command
|
||||||
def control_off(self):
|
def control_off(self):
|
||||||
"""switch control off"""
|
"""switch control off"""
|
||||||
|
@ -198,6 +198,10 @@ class MotorValve(PersistentMixin, Drivable):
|
|||||||
|
|
||||||
@Command
|
@Command
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current position
|
||||||
|
|
||||||
|
state will probably be undefined
|
||||||
|
"""
|
||||||
self._state.stop()
|
self._state.stop()
|
||||||
self.motor.stop()
|
self.motor.stop()
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"""modules to access parameters"""
|
"""modules to access parameters"""
|
||||||
|
|
||||||
from frappy.core import Drivable, EnumType, IDLE, Attached, StringType, Property, \
|
from frappy.core import Drivable, EnumType, IDLE, Attached, StringType, Property, \
|
||||||
Parameter, FloatRange, Readable, ERROR
|
Parameter, BoolType, FloatRange, Readable, ERROR, nopoll
|
||||||
from frappy.errors import ConfigError
|
from frappy.errors import ConfigError
|
||||||
from frappy_psi.convergence import HasConvergence
|
from frappy_psi.convergence import HasConvergence
|
||||||
from frappy_psi.mixins import HasRamp
|
from frappy_psi.mixins import HasRamp
|
||||||
@ -72,19 +72,21 @@ class Driv(Drivable):
|
|||||||
raise ConfigError('illegal recursive read/write module')
|
raise ConfigError('illegal recursive read/write module')
|
||||||
super().checkProperties()
|
super().checkProperties()
|
||||||
|
|
||||||
#def registerUpdates(self):
|
def registerUpdates(self):
|
||||||
# self.read.valueCallbacks[self.read_param].append(self.update_value)
|
self.read.addCallback(self.read_param, self.announceUpdate, 'value')
|
||||||
# self.write.valueCallbacks[self.write_param].append(self.update_target)
|
self.write.addCallback(self.write_param, self.announceUpdate, 'target')
|
||||||
#
|
|
||||||
#def startModule(self, start_events):
|
|
||||||
# start_events.queue(self.registerUpdates)
|
|
||||||
# super().startModule(start_events)
|
|
||||||
|
|
||||||
|
def startModule(self, start_events):
|
||||||
|
start_events.queue(self.registerUpdates)
|
||||||
|
super().startModule(start_events)
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return getattr(self.read, f'{self.read_param}')
|
return getattr(self.read, f'read_{self.read_param}')()
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return getattr(self.write, f'{self.write_param}')
|
return getattr(self.write, f'read_{self.write_param}')()
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
return IDLE, ''
|
return IDLE, ''
|
||||||
@ -130,7 +132,7 @@ def set_enabled(modobj, value):
|
|||||||
|
|
||||||
|
|
||||||
def get_value(obj, default):
|
def get_value(obj, default):
|
||||||
"""get the value of given module. if not valid, return the limit (min_high or max_low)"""
|
"""get the value of given module. if not valid, return the default"""
|
||||||
if not getattr(obj, 'enabled', True):
|
if not getattr(obj, 'enabled', True):
|
||||||
return default
|
return default
|
||||||
# consider also that a value 0 is invalid
|
# consider also that a value 0 is invalid
|
||||||
@ -148,14 +150,14 @@ class SwitchDriv(HasConvergence, Drivable):
|
|||||||
max_low = Parameter('maximum low target', FloatRange(unit='$'), readonly=False)
|
max_low = Parameter('maximum low target', FloatRange(unit='$'), readonly=False)
|
||||||
# disable_other = Parameter('whether to disable unused channel', BoolType(), readonly=False)
|
# disable_other = Parameter('whether to disable unused channel', BoolType(), readonly=False)
|
||||||
selected = Parameter('selected module', EnumType(low=LOW, high=HIGH), readonly=False, default=0)
|
selected = Parameter('selected module', EnumType(low=LOW, high=HIGH), readonly=False, default=0)
|
||||||
_switch_target = None # if not None, switch to selection mhen mid range is reached
|
autoswitch = Parameter('switch sensor automatically', BoolType(), readonly=False, default=True)
|
||||||
|
_switch_target = None # if not None, switch to selection when mid range is reached
|
||||||
|
|
||||||
# TODO: copy units from attached module
|
# TODO: copy units from attached module
|
||||||
# TODO: callbacks for updates
|
# TODO: callbacks for updates
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
super().doPoll()
|
super().doPoll()
|
||||||
if self.isBusy():
|
|
||||||
if self._switch_target is not None:
|
if self._switch_target is not None:
|
||||||
mid = (self.min_high + self.max_low) * 0.5
|
mid = (self.min_high + self.max_low) * 0.5
|
||||||
if self._switch_target == HIGH:
|
if self._switch_target == HIGH:
|
||||||
@ -166,33 +168,43 @@ class SwitchDriv(HasConvergence, Drivable):
|
|||||||
self.write_target(self.target)
|
self.write_target(self.target)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
high = get_value(self.high, mid) # return mid then high is invalid
|
high = get_value(self.high, mid) # return mid when high is invalid
|
||||||
if high < mid:
|
if high < mid: # change to self.max_low
|
||||||
self.value = self.high.value
|
self.value = self.high.value
|
||||||
self._switch_target = None
|
self._switch_target = None
|
||||||
self.write_target(self.target)
|
self.write_target(self.target)
|
||||||
return
|
return
|
||||||
else:
|
if not self.isBusy() and self.autoswitch:
|
||||||
low = get_value(self.low, self.max_low)
|
low = get_value(self.low, self.max_low)
|
||||||
high = get_value(self.high, self.min_high)
|
high = get_value(self.high, self.min_high)
|
||||||
low_valid = low < self.max_low
|
low_valid = low < self.max_low
|
||||||
high_valid = high > self.min_high
|
high_valid = high > self.min_high
|
||||||
if high_valid and high > self.max_low:
|
if high_valid and high > self.max_low:
|
||||||
if not low_valid:
|
if not low_valid and not self.low.control_active:
|
||||||
set_enabled(self.low, False)
|
set_enabled(self.low, False)
|
||||||
return
|
return
|
||||||
if low_valid and low < self.min_high:
|
if low_valid and low < self.min_high:
|
||||||
if not high_valid:
|
if not high_valid and not self.high.control_active:
|
||||||
set_enabled(self.high, False)
|
set_enabled(self.high, False)
|
||||||
return
|
return
|
||||||
set_enabled(self.low, True)
|
# keep only one channel on
|
||||||
set_enabled(self.high, True)
|
#set_enabled(self.low, True)
|
||||||
|
#set_enabled(self.high, True)
|
||||||
|
|
||||||
|
def get_selected(self):
|
||||||
|
low = get_value(self.low, self.max_low)
|
||||||
|
high = get_value(self.high, self.min_high)
|
||||||
|
if low < self.min_high:
|
||||||
|
return 0
|
||||||
|
if high > self.max_low:
|
||||||
|
return 1
|
||||||
|
return self.selected
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.low.value if self.selected == LOW else self.high.value
|
return self.low.value if self.get_selected() == LOW else self.high.value
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
status = self.low.status if self.selected == LOW else self.high.status
|
status = self.low.status if self.get_selected() == LOW else self.high.status
|
||||||
if status[0] >= ERROR:
|
if status[0] >= ERROR:
|
||||||
return status
|
return status
|
||||||
return super().read_status() # convergence status
|
return super().read_status() # convergence status
|
||||||
@ -202,21 +214,22 @@ class SwitchDriv(HasConvergence, Drivable):
|
|||||||
selected = self.selected
|
selected = self.selected
|
||||||
target1 = target
|
target1 = target
|
||||||
self._switch_target = None
|
self._switch_target = None
|
||||||
if target > self.max_low:
|
if target > self.max_low * 0.75 + self.min_high * 0.25:
|
||||||
if self.value < self.min_high:
|
if self.value < self.min_high:
|
||||||
target1 = self.max_low
|
target1 = min(target, self.max_low)
|
||||||
self._switch_target = HIGH
|
self._switch_target = HIGH
|
||||||
selected = LOW
|
selected = LOW
|
||||||
else:
|
else:
|
||||||
this, other = other, this
|
this, other = other, this
|
||||||
selected = HIGH
|
selected = HIGH
|
||||||
elif target < self.min_high:
|
elif target < self.min_high * 0.75 + self.max_low * 0.25:
|
||||||
if self.value > self.max_low:
|
# reinstall this with higher threshold (e.g. 4 K)?
|
||||||
target1 = self.min_high
|
#if self.value > self.max_low:
|
||||||
self._switch_target = LOW
|
# target1 = max(self.min_high, target)
|
||||||
this, other = other, this
|
# self._switch_target = LOW
|
||||||
selected = HIGH
|
# this, other = other, this
|
||||||
else:
|
# selected = HIGH
|
||||||
|
#else:
|
||||||
selected = LOW
|
selected = LOW
|
||||||
elif self.selected == HIGH:
|
elif self.selected == HIGH:
|
||||||
this, other = other, this
|
this, other = other, this
|
||||||
|
@ -62,7 +62,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
|
|
||||||
encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2),
|
encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2),
|
||||||
default=1, readonly=False)
|
default=1, readonly=False)
|
||||||
check_limit_switches = Parameter('whethter limit switches are checked',BoolType(),
|
check_limit_switches = Parameter('whether limit switches are checked',BoolType(),
|
||||||
default=0, readonly=False)
|
default=0, readonly=False)
|
||||||
value = PersistentParam('angle', FloatRange(unit='deg'))
|
value = PersistentParam('angle', FloatRange(unit='deg'))
|
||||||
status = PersistentParam()
|
status = PersistentParam()
|
||||||
@ -90,6 +90,8 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
status_bits = ['power stage error', 'undervoltage', 'overtemperature', 'active',
|
status_bits = ['power stage error', 'undervoltage', 'overtemperature', 'active',
|
||||||
'lower switch active', 'upper switch active', 'step failure', 'encoder error']
|
'lower switch active', 'upper switch active', 'step failure', 'encoder error']
|
||||||
|
|
||||||
|
_doing_reference = False
|
||||||
|
|
||||||
def get(self, cmd):
|
def get(self, cmd):
|
||||||
return self.communicate(f'{self.address:x}{self.axis}{cmd}')
|
return self.communicate(f'{self.address:x}{self.axis}{cmd}')
|
||||||
|
|
||||||
@ -178,10 +180,14 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
super().doPoll()
|
super().doPoll()
|
||||||
if self._running and not self.isBusy():
|
if self._running and not self.isBusy() and not self._doing_reference:
|
||||||
if time.time() > self._stopped_at + 5:
|
if time.time() > self._stopped_at + 5:
|
||||||
self.log.warning('stop motor not started by us')
|
self.log.warning('stop motor not started by us')
|
||||||
self.hw_stop()
|
self.hw_stop()
|
||||||
|
if self._doing_reference and self.get('=H') == 'E' :
|
||||||
|
self.status = IDLE, ''
|
||||||
|
self.target = 0
|
||||||
|
self._doing_reference = False
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
hexstatus = 0x100
|
hexstatus = 0x100
|
||||||
@ -207,6 +213,9 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
if status[0] == ERROR:
|
if status[0] == ERROR:
|
||||||
self._blocking_error = status[1]
|
self._blocking_error = status[1]
|
||||||
return status
|
return status
|
||||||
|
if self._doing_reference and self.get('=H') == 'N':
|
||||||
|
status = BUSY, 'Doing reference run'
|
||||||
|
return status
|
||||||
return super().read_status() # status from state machine
|
return super().read_status() # status from state machine
|
||||||
|
|
||||||
def check_moving(self):
|
def check_moving(self):
|
||||||
@ -346,3 +355,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
self.status = 'IDLE', 'after error reset'
|
self.status = 'IDLE', 'after error reset'
|
||||||
self._blocking_error = None
|
self._blocking_error = None
|
||||||
self.target = self.value # clear error in target
|
self.target = self.value # clear error in target
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def make_ref_run(self):
|
||||||
|
'''Do reference run'''
|
||||||
|
self._doing_reference = True
|
||||||
|
self.status = BUSY, 'Doing reference run'
|
||||||
|
self.communicate(f'{self.address:x}{self.axis}0-')
|
||||||
|
@ -483,6 +483,10 @@ class Temp(PpmsDrivable):
|
|||||||
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""set setpoint to current value
|
||||||
|
|
||||||
|
but restrict to values between last target and current target
|
||||||
|
"""
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
if self.status[0] != StatusType.STABILIZING:
|
if self.status[0] != StatusType.STABILIZING:
|
||||||
@ -612,6 +616,7 @@ class Field(PpmsDrivable):
|
|||||||
# do not execute FIELD command, as this would trigger a ramp up of leads current
|
# do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current driven Field"""
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
@ -714,6 +719,7 @@ class Position(PpmsDrivable):
|
|||||||
return value # do not execute MOVE command, as this would trigger an unnecessary move
|
return value # do not execute MOVE command, as this would trigger an unnecessary move
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop motor"""
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
# *****************************************************************************
|
|
||||||
# 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 <oksana.shliakhtun@psi.ch>
|
|
||||||
# *****************************************************************************
|
|
||||||
"""Keithley Instruments 2601B-PULSE System (not finished)"""
|
|
||||||
|
|
||||||
from frappy.core import StringIO, HasIO, Readable, Writable, \
|
|
||||||
Parameter, FloatRange, EnumType
|
|
||||||
|
|
||||||
|
|
||||||
class PulseIO(StringIO):
|
|
||||||
end_of_line = '\n'
|
|
||||||
identification = [('*IDN?', 'Keithley Instruments, Model 2601B-PULSE,.*')]
|
|
||||||
|
|
||||||
|
|
||||||
class Base(HasIO):
|
|
||||||
|
|
||||||
def set_source(self):
|
|
||||||
"""
|
|
||||||
Set the source -always current
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return self.communicate(f'smua.source.func = smua.OUTPUT_DCAMPS')
|
|
||||||
|
|
||||||
def get_par(self, cmd):
|
|
||||||
return self.communicate(f'reading = smua.measure.{cmd}()')
|
|
||||||
|
|
||||||
def auto_onof(self, val):
|
|
||||||
if val == 1:
|
|
||||||
return f'smua.AUTORANGE_ON'
|
|
||||||
if val == 0:
|
|
||||||
return f'smua.AUTORANGE_OFF'
|
|
||||||
|
|
||||||
def set_measure(self, cmd, val):
|
|
||||||
return self.communicate(f'smua.measure.{cmd} = {val}')
|
|
||||||
|
|
||||||
|
|
||||||
class Create_Pulse(Base, Readable, Writable):
|
|
||||||
target = Parameter('source target', FloatRange, unit='A', readonly=False)
|
|
||||||
width = Parameter('pulse width', FloatRange, unit="s", readonly=False)
|
|
||||||
resistance = Parameter('resistance', FloatRange)
|
|
||||||
|
|
||||||
SOURCE_RANGE = ['100nA', '1uA', '10uA', '100uA', '1mA', '10mA', '100mA', '1A', '3A']
|
|
||||||
range = Parameter('source range', EnumType('source range',
|
|
||||||
{name: idx for idx, name in enumerate(SOURCE_RANGE)}), readonly=False)
|
|
||||||
|
|
||||||
def read_range(self):
|
|
||||||
return self.range
|
|
||||||
|
|
||||||
def write_range(self, range):
|
|
||||||
self.communicate(f'smua.source.rangei = {range}')
|
|
||||||
return self.range
|
|
||||||
|
|
||||||
def write_target(self, target):
|
|
||||||
return self.communicate(f'smua.source.leveli = {target}')
|
|
||||||
|
|
||||||
def read_resistance(self):
|
|
||||||
return self.communicate('reading = smua.measure.r()')
|
|
||||||
|
|
||||||
|
|
||||||
class Script(Create_Pulse):
|
|
@ -16,7 +16,7 @@
|
|||||||
# Module authors:
|
# Module authors:
|
||||||
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Temperature Controller TC1 Quantum NorthWest"""
|
|
||||||
|
|
||||||
from frappy.core import Readable, Parameter, FloatRange, IDLE, ERROR, BoolType,\
|
from frappy.core import Readable, Parameter, FloatRange, IDLE, ERROR, BoolType,\
|
||||||
StringIO, HasIO, Property, WARN, Drivable, BUSY, StringType, Done
|
StringIO, HasIO, Property, WARN, Drivable, BUSY, StringType, Done
|
||||||
@ -31,17 +31,10 @@ class QnwIO(StringIO):
|
|||||||
|
|
||||||
class SensorTC1(HasIO, Readable):
|
class SensorTC1(HasIO, Readable):
|
||||||
ioClass = QnwIO
|
ioClass = QnwIO
|
||||||
value = Parameter(unit='degC', min=-55, max=150)
|
value = Parameter(unit='degC', min=-15, max=120)
|
||||||
channel = Property('channel name', StringType())
|
channel = Property('channel name', StringType())
|
||||||
|
|
||||||
def set_param(self, adr, value=None):
|
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]
|
short = adr.split()[0]
|
||||||
# try 3 times in case we got an asynchronous message
|
# try 3 times in case we got an asynchronous message
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
@ -73,7 +66,7 @@ class SensorTC1(HasIO, Readable):
|
|||||||
|
|
||||||
class TemperatureLoopTC1(SensorTC1, Drivable):
|
class TemperatureLoopTC1(SensorTC1, Drivable):
|
||||||
value = Parameter('temperature', unit='degC')
|
value = Parameter('temperature', unit='degC')
|
||||||
target = Parameter('setpoint', unit='degC', min=-55, max=150)
|
target = Parameter('setpoint', unit='degC', min=-5, max=110)
|
||||||
control = Parameter('temperature control flag', BoolType(), readonly=False)
|
control = Parameter('temperature control flag', BoolType(), readonly=False)
|
||||||
ramp = Parameter('ramping value', FloatRange, unit='degC/min', readonly=False)
|
ramp = Parameter('ramping value', FloatRange, unit='degC/min', readonly=False)
|
||||||
ramp_used = Parameter('ramping status', BoolType(), default=False, readonly=False)
|
ramp_used = Parameter('ramping status', BoolType(), default=False, readonly=False)
|
||||||
@ -87,16 +80,6 @@ class TemperatureLoopTC1(SensorTC1, Drivable):
|
|||||||
return self.get_param('MT')
|
return self.get_param('MT')
|
||||||
|
|
||||||
def read_status(self):
|
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()
|
status = super().read_status()
|
||||||
if status[0] == ERROR:
|
if status[0] == ERROR:
|
||||||
return status
|
return status
|
||||||
@ -158,5 +141,6 @@ class TemperatureLoopTC1(SensorTC1, Drivable):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop at current value (does nothing if ramp is not used)"""
|
||||||
if self.control and self.ramp_used:
|
if self.control and self.ramp_used:
|
||||||
self.write_target(self.value)
|
self.write_target(self.value)
|
||||||
|
70
frappy_psi/rheo_trigger.py
Normal file
70
frappy_psi/rheo_trigger.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from frappy.core import StringType, BoolType, Parameter, Property, PersistentParam, Command, IDLE, ERROR, WARN, Writable
|
||||||
|
import time
|
||||||
|
|
||||||
|
class RheoTrigger(Writable):
|
||||||
|
addr = Property('Port address', StringType())
|
||||||
|
value = Parameter('Output state', BoolType(), default=0)
|
||||||
|
target = Parameter('target', BoolType(), default=0, readonly=False)
|
||||||
|
status = Parameter()
|
||||||
|
doBeep = Property('Make noise', BoolType(), default=0)
|
||||||
|
|
||||||
|
_status = 0
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
with open('/sys/class/ionopimax/digital_io/'+self.addr+'_mode', 'w') as f :
|
||||||
|
f.write('out')
|
||||||
|
|
||||||
|
if self.doBeep:
|
||||||
|
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
|
||||||
|
f.write('200 50 3')
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
with open('/sys/class/ionopimax/digital_io/'+self.addr, 'r') as f :
|
||||||
|
file_value = f.read()
|
||||||
|
if file_value == '0\n':
|
||||||
|
value = False
|
||||||
|
self._status = 0
|
||||||
|
elif file_value == '1\n':
|
||||||
|
value = True
|
||||||
|
self._status = 1
|
||||||
|
else:
|
||||||
|
self._status = -1
|
||||||
|
value = False
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def write_target(self,target):
|
||||||
|
if target == self.value:
|
||||||
|
return target
|
||||||
|
else:
|
||||||
|
with open('/sys/class/ionopimax/digital_io/'+self.addr, 'w') as f :
|
||||||
|
if target == True:
|
||||||
|
f.write('1')
|
||||||
|
elif target == False:
|
||||||
|
f.write('0')
|
||||||
|
time.sleep(0.05)
|
||||||
|
if self.doBeep:
|
||||||
|
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
|
||||||
|
f.write('200')
|
||||||
|
self.status = self.read_status()
|
||||||
|
return target
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
self.value = self.read_value()
|
||||||
|
if self._status == 0:
|
||||||
|
return IDLE, 'Signal low'
|
||||||
|
elif self._status == 1:
|
||||||
|
return IDLE, 'Signal high'
|
||||||
|
else:
|
||||||
|
return ERROR, 'Cannot read status'
|
||||||
|
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def toggle(self):
|
||||||
|
"""Toggle output"""
|
||||||
|
value = self.read_value()
|
||||||
|
if value == True:
|
||||||
|
self.write_target(False)
|
||||||
|
else:
|
||||||
|
self.write_target(True)
|
@ -39,12 +39,13 @@ from os.path import expanduser, join, exists
|
|||||||
from frappy.client import ProxyClient
|
from frappy.client import ProxyClient
|
||||||
from frappy.datatypes import ArrayOf, BoolType, \
|
from frappy.datatypes import ArrayOf, BoolType, \
|
||||||
EnumType, FloatRange, IntRange, StringType
|
EnumType, FloatRange, IntRange, StringType
|
||||||
from frappy.errors import ConfigError, HardwareError, secop_error, CommunicationFailedError
|
from frappy.core import IDLE, BUSY, ERROR
|
||||||
|
from frappy.errors import ConfigError, HardwareError, CommunicationFailedError
|
||||||
from frappy.lib import generalConfig, mkthread
|
from frappy.lib import generalConfig, mkthread
|
||||||
from frappy.lib.asynconn import AsynConn, ConnectionClosed
|
from frappy.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
from frappy.modules import Attached, Command, Done, Drivable, \
|
from frappy.modulebase import Done
|
||||||
|
from frappy.modules import Attached, Command, Drivable, \
|
||||||
Module, Parameter, Property, Readable, Writable
|
Module, Parameter, Property, Readable, Writable
|
||||||
from frappy.protocol.dispatcher import make_update
|
|
||||||
|
|
||||||
|
|
||||||
CFG_HEADER = """Node('%(config)s.sea.psi.ch',
|
CFG_HEADER = """Node('%(config)s.sea.psi.ch',
|
||||||
@ -107,7 +108,6 @@ class SeaClient(ProxyClient, Module):
|
|||||||
service = Property("main/stick/addons", StringType(), default='')
|
service = Property("main/stick/addons", StringType(), default='')
|
||||||
visibility = 'expert'
|
visibility = 'expert'
|
||||||
default_json_file = {}
|
default_json_file = {}
|
||||||
_connect_thread = None
|
|
||||||
_instance = None
|
_instance = None
|
||||||
_last_connect = 0
|
_last_connect = 0
|
||||||
|
|
||||||
@ -124,6 +124,8 @@ class SeaClient(ProxyClient, Module):
|
|||||||
self.shutdown = False
|
self.shutdown = False
|
||||||
self.path2param = {}
|
self.path2param = {}
|
||||||
self._write_lock = threading.Lock()
|
self._write_lock = threading.Lock()
|
||||||
|
self._connect_thread = None
|
||||||
|
self._connected = False
|
||||||
config = opts.get('config')
|
config = opts.get('config')
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
config = config['value']
|
config = config['value']
|
||||||
@ -135,14 +137,11 @@ class SeaClient(ProxyClient, Module):
|
|||||||
Module.__init__(self, name, log, opts, srv)
|
Module.__init__(self, name, log, opts, srv)
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
if not self.asynio and time.time() > self._last_connect + 10:
|
if not self._connected and time.time() > self._last_connect + 10:
|
||||||
with self._write_lock:
|
|
||||||
# make sure no more connect thread is running
|
|
||||||
if self._connect_thread and self._connect_thread.isAlive():
|
|
||||||
return
|
|
||||||
if not self._last_connect:
|
if not self._last_connect:
|
||||||
self.log.info('reconnect to SEA %s', self.service)
|
self.log.info('reconnect to SEA %s', self.service)
|
||||||
self._connect_thread = mkthread(self._connect, None)
|
if self._connect_thread is None:
|
||||||
|
self._connect_thread = mkthread(self._connect)
|
||||||
|
|
||||||
def register_obj(self, module, obj):
|
def register_obj(self, module, obj):
|
||||||
self.objects.add(obj)
|
self.objects.add(obj)
|
||||||
@ -150,15 +149,13 @@ class SeaClient(ProxyClient, Module):
|
|||||||
self.path2param.setdefault(k, []).extend(v)
|
self.path2param.setdefault(k, []).extend(v)
|
||||||
self.register_callback(module.name, module.updateEvent)
|
self.register_callback(module.name, module.updateEvent)
|
||||||
|
|
||||||
def _connect(self, started_callback):
|
def _connect(self):
|
||||||
self.asynio = None
|
try:
|
||||||
if self.syncio:
|
if self.syncio:
|
||||||
# trigger syncio reconnect in self.request()
|
|
||||||
try:
|
try:
|
||||||
self.syncio.disconnect()
|
self.syncio.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.syncio = None
|
|
||||||
self._last_connect = time.time()
|
self._last_connect = time.time()
|
||||||
if self._instance:
|
if self._instance:
|
||||||
try:
|
try:
|
||||||
@ -179,32 +176,34 @@ class SeaClient(ProxyClient, Module):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise CommunicationFailedError('reply %r should be "Login OK"' % reply)
|
raise CommunicationFailedError('reply %r should be "Login OK"' % reply)
|
||||||
result = self.request('frappy_config %s %s' % (self.service, self.config))
|
|
||||||
if result.startswith('ERROR:'):
|
|
||||||
raise CommunicationFailedError(f'reply from frappy_config: {result}')
|
|
||||||
# frappy_async_client switches to the json protocol (better for updates)
|
|
||||||
self.asynio.writeline(b'frappy_async_client')
|
|
||||||
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
|
|
||||||
self._connect_thread = None
|
|
||||||
mkthread(self._rxthread, started_callback)
|
|
||||||
|
|
||||||
def request(self, command, quiet=False):
|
|
||||||
"""send a request and wait for reply"""
|
|
||||||
with self._write_lock:
|
|
||||||
if not self.syncio or not self.syncio.connection:
|
|
||||||
if not self.asynio or not self.asynio.connection:
|
|
||||||
try:
|
|
||||||
self._connect_thread.join()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
# let doPoll do the reconnect
|
|
||||||
self.pollInfo.trigger(True)
|
|
||||||
raise ConnectionClosed('disconnected - reconnect later')
|
|
||||||
self.syncio = AsynConn(self.uri)
|
self.syncio = AsynConn(self.uri)
|
||||||
assert self.syncio.readline() == b'OK'
|
assert self.syncio.readline() == b'OK'
|
||||||
self.syncio.writeline(b'seauser seaser')
|
self.syncio.writeline(b'seauser seaser')
|
||||||
assert self.syncio.readline() == b'Login OK'
|
assert self.syncio.readline() == b'Login OK'
|
||||||
self.log.info('connected to %s', self.uri)
|
self.log.info('connected to %s', self.uri)
|
||||||
|
|
||||||
|
result = self.raw_request('frappy_config %s %s' % (self.service, self.config))
|
||||||
|
if result.startswith('ERROR:'):
|
||||||
|
raise CommunicationFailedError(f'reply from frappy_config: {result}')
|
||||||
|
# frappy_async_client switches to the json protocol (better for updates)
|
||||||
|
self.asynio.writeline(b'frappy_async_client')
|
||||||
|
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
|
||||||
|
self._connected = True
|
||||||
|
mkthread(self._rxthread)
|
||||||
|
finally:
|
||||||
|
self._connect_thread = None
|
||||||
|
|
||||||
|
def request(self, command, quiet=False):
|
||||||
|
with self._write_lock:
|
||||||
|
if not self._connected:
|
||||||
|
if self._connect_thread is None:
|
||||||
|
# let doPoll do the reconnect
|
||||||
|
self.pollInfo.trigger(True)
|
||||||
|
raise ConnectionClosed('disconnected - reconnect is tried later')
|
||||||
|
return self.raw_request(command, quiet)
|
||||||
|
|
||||||
|
def raw_request(self, command, quiet=False):
|
||||||
|
"""send a request and wait for reply"""
|
||||||
try:
|
try:
|
||||||
self.syncio.flush_recv()
|
self.syncio.flush_recv()
|
||||||
ft = 'fulltransAct' if quiet else 'fulltransact'
|
ft = 'fulltransAct' if quiet else 'fulltransact'
|
||||||
@ -233,16 +232,22 @@ class SeaClient(ProxyClient, Module):
|
|||||||
result = [reply.split('=', 1)[-1]]
|
result = [reply.split('=', 1)[-1]]
|
||||||
else:
|
else:
|
||||||
result.append(reply)
|
result.append(reply)
|
||||||
|
raise TimeoutError('no response within 10s')
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
|
self.close_connections()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
connections = self.syncio, self.asynio
|
||||||
|
self._connected = False
|
||||||
|
self.syncio = self.asynio = None
|
||||||
|
for conn in connections:
|
||||||
try:
|
try:
|
||||||
self.syncio.disconnect()
|
conn.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.syncio = None
|
|
||||||
raise
|
|
||||||
raise TimeoutError('no response within 10s')
|
|
||||||
|
|
||||||
def _rxthread(self, started_callback):
|
def _rxthread(self):
|
||||||
recheck = None
|
recheck = None
|
||||||
while not self.shutdown:
|
while not self.shutdown:
|
||||||
if recheck and time.time() > recheck:
|
if recheck and time.time() > recheck:
|
||||||
@ -258,11 +263,7 @@ class SeaClient(ProxyClient, Module):
|
|||||||
if reply is None:
|
if reply is None:
|
||||||
continue
|
continue
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
try:
|
self.close_connections()
|
||||||
self.asynio.disconnect()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.asynio = None
|
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
msg = json.loads(reply)
|
msg = json.loads(reply)
|
||||||
@ -289,9 +290,6 @@ class SeaClient(ProxyClient, Module):
|
|||||||
data = msg['data']
|
data = msg['data']
|
||||||
if flag == 'finish' and obj == 'get_all_param':
|
if flag == 'finish' and obj == 'get_all_param':
|
||||||
# first updates have finished
|
# first updates have finished
|
||||||
if started_callback:
|
|
||||||
started_callback()
|
|
||||||
started_callback = None
|
|
||||||
continue
|
continue
|
||||||
if flag != 'hdbevent':
|
if flag != 'hdbevent':
|
||||||
if obj not in ('frappy_async_client', 'get_all_param'):
|
if obj not in ('frappy_async_client', 'get_all_param'):
|
||||||
@ -352,7 +350,7 @@ class SeaClient(ProxyClient, Module):
|
|||||||
class SeaConfigCreator(SeaClient):
|
class SeaConfigCreator(SeaClient):
|
||||||
def startModule(self, start_events):
|
def startModule(self, start_events):
|
||||||
"""save objects (and sub-objects) description and exit"""
|
"""save objects (and sub-objects) description and exit"""
|
||||||
self._connect(None)
|
self._connect()
|
||||||
reply = self.request('describe_all')
|
reply = self.request('describe_all')
|
||||||
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
|
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
|
||||||
description, reply = json.loads(reply)
|
description, reply = json.loads(reply)
|
||||||
@ -644,22 +642,7 @@ class SeaModule(Module):
|
|||||||
if upd:
|
if upd:
|
||||||
upd(value, timestamp, readerror)
|
upd(value, timestamp, readerror)
|
||||||
return
|
return
|
||||||
try:
|
self.announceUpdate(parameter, value, readerror, timestamp)
|
||||||
pobj = self.parameters[parameter]
|
|
||||||
except KeyError:
|
|
||||||
self.log.error('do not know %s:%s', self.name, parameter)
|
|
||||||
raise
|
|
||||||
pobj.timestamp = timestamp
|
|
||||||
# should be done here: deal with clock differences
|
|
||||||
if not readerror:
|
|
||||||
try:
|
|
||||||
pobj.value = value # store the value even in case of a validation error
|
|
||||||
pobj.value = pobj.datatype(value)
|
|
||||||
except Exception as e:
|
|
||||||
readerror = secop_error(e)
|
|
||||||
pobj.readerror = readerror
|
|
||||||
if pobj.export:
|
|
||||||
self.secNode.srv.dispatcher.broadcast_event(make_update(self.name, pobj))
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self.io.register_obj(self, self.sea_object)
|
self.io.register_obj(self, self.sea_object)
|
||||||
@ -670,20 +653,35 @@ class SeaModule(Module):
|
|||||||
|
|
||||||
|
|
||||||
class SeaReadable(SeaModule, Readable):
|
class SeaReadable(SeaModule, Readable):
|
||||||
|
_readerror = None
|
||||||
|
_status = IDLE, ''
|
||||||
|
|
||||||
|
def update_value(self, value, timestamp, readerror):
|
||||||
|
# make sure status is always ERROR when reading value fails
|
||||||
|
self._readerror = readerror
|
||||||
|
if readerror:
|
||||||
|
self.read_status() # forced ERROR status
|
||||||
|
self.announceUpdate('value', value, readerror, timestamp)
|
||||||
|
else: # order is important
|
||||||
|
self.value = value # includes announceUpdate
|
||||||
|
self.read_status() # send event for ordinary self._status
|
||||||
|
|
||||||
def update_status(self, value, timestamp, readerror):
|
def update_status(self, value, timestamp, readerror):
|
||||||
if readerror:
|
if readerror:
|
||||||
value = repr(readerror)
|
value = f'{readerror.name} - {readerror}'
|
||||||
if value == '':
|
if value == '':
|
||||||
self.status = (self.Status.IDLE, '')
|
self._status = IDLE, ''
|
||||||
else:
|
else:
|
||||||
self.status = (self.Status.ERROR, value)
|
self._status = ERROR, value
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
return self.status
|
if self._readerror:
|
||||||
|
return ERROR, f'{self._readerror.name} - {self._readerror}'
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
|
||||||
class SeaWritable(SeaModule, Writable):
|
class SeaWritable(SeaReadable, Writable):
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.target
|
return self.target
|
||||||
|
|
||||||
@ -693,20 +691,13 @@ class SeaWritable(SeaModule, Writable):
|
|||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
class SeaDrivable(SeaModule, Drivable):
|
class SeaDrivable(SeaReadable, Drivable):
|
||||||
_sea_status = ''
|
|
||||||
_is_running = 0
|
_is_running = 0
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
super().earlyInit()
|
super().earlyInit()
|
||||||
self._run_event = threading.Event()
|
self._run_event = threading.Event()
|
||||||
|
|
||||||
def read_status(self):
|
|
||||||
return self.status
|
|
||||||
|
|
||||||
# def read_target(self):
|
|
||||||
# return self.target
|
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
self._run_event.clear()
|
self._run_event.clear()
|
||||||
self.io.query(f'run {self.sea_object} {value}')
|
self.io.query(f'run {self.sea_object} {value}')
|
||||||
@ -714,25 +705,20 @@ class SeaDrivable(SeaModule, Drivable):
|
|||||||
self.log.warn('target changed but is_running stays 0')
|
self.log.warn('target changed but is_running stays 0')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def update_status(self, value, timestamp, readerror):
|
|
||||||
if not readerror:
|
|
||||||
self._sea_status = value
|
|
||||||
self.updateStatus()
|
|
||||||
|
|
||||||
def update_is_running(self, value, timestamp, readerror):
|
def update_is_running(self, value, timestamp, readerror):
|
||||||
if not readerror:
|
if not readerror:
|
||||||
self._is_running = value
|
self._is_running = value
|
||||||
self.updateStatus()
|
self.read_status()
|
||||||
if value:
|
if value:
|
||||||
self._run_event.set()
|
self._run_event.set()
|
||||||
|
|
||||||
def updateStatus(self):
|
def read_status(self):
|
||||||
if self._sea_status:
|
status = super().read_status()
|
||||||
self.status = (self.Status.ERROR, self._sea_status)
|
if self._is_running:
|
||||||
elif self._is_running:
|
if status[0] >= ERROR:
|
||||||
self.status = (self.Status.BUSY, 'driving')
|
return ERROR, 'BUSY + ' + status[1]
|
||||||
else:
|
return BUSY, 'driving'
|
||||||
self.status = (self.Status.IDLE, '')
|
return status
|
||||||
|
|
||||||
def updateTarget(self, module, parameter, value, timestamp, readerror):
|
def updateTarget(self, module, parameter, value, timestamp, readerror):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
@ -27,7 +27,7 @@ import numpy as np
|
|||||||
from scipy.interpolate import splev, splrep # pylint: disable=import-error
|
from scipy.interpolate import splev, splrep # pylint: disable=import-error
|
||||||
|
|
||||||
from frappy.core import Attached, BoolType, Parameter, Readable, StringType, \
|
from frappy.core import Attached, BoolType, Parameter, Readable, StringType, \
|
||||||
FloatRange
|
FloatRange, nopoll
|
||||||
|
|
||||||
|
|
||||||
def linear(x):
|
def linear(x):
|
||||||
@ -195,35 +195,40 @@ class Sensor(Readable):
|
|||||||
if self.description == '_':
|
if self.description == '_':
|
||||||
self.description = f'{self.rawsensor!r} calibrated with curve {self.calib!r}'
|
self.description = f'{self.rawsensor!r} calibrated with curve {self.calib!r}'
|
||||||
|
|
||||||
def doPoll(self):
|
|
||||||
self.read_status()
|
|
||||||
|
|
||||||
def write_calib(self, value):
|
def write_calib(self, value):
|
||||||
self._calib = CalCurve(value)
|
self._calib = CalCurve(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def update_value(self, value):
|
def _get_value(self, rawvalue):
|
||||||
if self.abs:
|
if self.abs:
|
||||||
value = abs(float(value))
|
rawvalue = abs(float(rawvalue))
|
||||||
self.value = self._calib(value)
|
return self._calib(rawvalue)
|
||||||
self._value_error = None
|
|
||||||
|
|
||||||
def error_update_value(self, err):
|
def _get_status(self, rawstatus):
|
||||||
|
return rawstatus if self._value_error is None else (self.Status.ERROR, self._value_error)
|
||||||
|
|
||||||
|
def update_value(self, rawvalue, err=None):
|
||||||
|
if err:
|
||||||
if self.abs and str(err) == 'R_UNDER': # hack: ignore R_UNDER from ls370
|
if self.abs and str(err) == 'R_UNDER': # hack: ignore R_UNDER from ls370
|
||||||
self._value_error = None
|
self._value_error = None
|
||||||
return None
|
return
|
||||||
self._value_error = repr(err)
|
err = repr(err)
|
||||||
raise err
|
|
||||||
|
|
||||||
def update_status(self, value):
|
|
||||||
if self._value_error is None:
|
|
||||||
self.status = value
|
|
||||||
else:
|
else:
|
||||||
self.status = self.Status.ERROR, self._value_error
|
try:
|
||||||
|
self.value = self._get_value(rawvalue)
|
||||||
|
except Exception as e:
|
||||||
|
err = repr(e)
|
||||||
|
if err != self._value_error:
|
||||||
|
self._value_error = err
|
||||||
|
self.status = self._get_status(self.rawsensor.status)
|
||||||
|
|
||||||
|
def update_status(self, rawstatus):
|
||||||
|
self.status = self._get_status(rawstatus)
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._calib(self.rawsensor.read_value())
|
return self._get_value(self.rawsensor.read_value())
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
self.update_status(self.rawsensor.status)
|
return self._get_status(self.rawsensor.read_status())
|
||||||
return self.status
|
|
||||||
|
@ -26,6 +26,7 @@ from frappy.datatypes import EnumType, FloatRange, StringType
|
|||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput
|
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput
|
||||||
from frappy_psi import mercury
|
from frappy_psi import mercury
|
||||||
|
from frappy_psi.frozenparam import FrozenParam
|
||||||
|
|
||||||
actions = Enum(none=0, condense=1, circulate=2, collect=3)
|
actions = Enum(none=0, condense=1, circulate=2, collect=3)
|
||||||
open_close = Mapped(CLOSE=0, OPEN=1)
|
open_close = Mapped(CLOSE=0, OPEN=1)
|
||||||
@ -39,22 +40,23 @@ class Action(MercuryChannel, Writable):
|
|||||||
mix_channel = Property('mix channel', StringType(), 'T5')
|
mix_channel = Property('mix channel', StringType(), 'T5')
|
||||||
still_channel = Property('cool down channel', StringType(), 'T4')
|
still_channel = Property('cool down channel', StringType(), 'T4')
|
||||||
value = Parameter('running action', EnumType(actions))
|
value = Parameter('running action', EnumType(actions))
|
||||||
target = Parameter('action to do', EnumType(none=0, condense=1, collect=3), readonly=False)
|
target = FrozenParam('action to do', EnumType(none=0, condense=1, collect=3), readonly=False)
|
||||||
_target = 0
|
_target = 0
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.query('SYS:DR:ACTN', actions_map)
|
return self.query('SYS:DR:ACTN', actions_map)
|
||||||
|
|
||||||
def read_target(self):
|
# as target is a FrozenParam, value might be still lag behind target
|
||||||
return self._target
|
# but will be updated when changed from an other source
|
||||||
|
read_target = read_value
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
self._target = value
|
|
||||||
self.change('SYS:DR:CHAN:COOL', self.cooldown_channel, str)
|
self.change('SYS:DR:CHAN:COOL', self.cooldown_channel, str)
|
||||||
self.change('SYS:DR:CHAN:STIL', self.still_channel, str)
|
self.change('SYS:DR:CHAN:STIL', self.still_channel, str)
|
||||||
self.change('SYS:DR:CHAN:MC', self.mix_channel, str)
|
self.change('SYS:DR:CHAN:MC', self.mix_channel, str)
|
||||||
self.change('DEV:T5:TEMP:MEAS:ENAB', 'ON', str)
|
self.change('DEV:T5:TEMP:MEAS:ENAB', 'ON', str)
|
||||||
return self.change('SYS:DR:ACTN', value, actions_map)
|
self.change('SYS:DR:ACTN', value, actions_map)
|
||||||
|
return value
|
||||||
|
|
||||||
# actions:
|
# actions:
|
||||||
# NONE (no action)
|
# NONE (no action)
|
||||||
@ -74,7 +76,7 @@ class Action(MercuryChannel, Writable):
|
|||||||
class Valve(MercuryChannel, Drivable):
|
class Valve(MercuryChannel, Drivable):
|
||||||
kind = 'VALV'
|
kind = 'VALV'
|
||||||
value = Parameter('valve state', EnumType(closed=0, opened=1))
|
value = Parameter('valve state', EnumType(closed=0, opened=1))
|
||||||
target = Parameter('valve target', EnumType(close=0, open=1))
|
target = FrozenParam('valve target', EnumType(close=0, open=1))
|
||||||
|
|
||||||
_try_count = None
|
_try_count = None
|
||||||
|
|
||||||
@ -108,6 +110,10 @@ class Valve(MercuryChannel, Drivable):
|
|||||||
self.change('DEV::VALV:SIG:STATE', self.target, open_close)
|
self.change('DEV::VALV:SIG:STATE', self.target, open_close)
|
||||||
return BUSY, 'waiting'
|
return BUSY, 'waiting'
|
||||||
|
|
||||||
|
# as target is a FrozenParam, value might be still lag behind target
|
||||||
|
# but will be updated when changed from an other source
|
||||||
|
read_target = read_value
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
if value != self.read_value():
|
if value != self.read_value():
|
||||||
self._try_count = 0
|
self._try_count = 0
|
||||||
@ -120,13 +126,18 @@ class Valve(MercuryChannel, Drivable):
|
|||||||
class Pump(MercuryChannel, Writable):
|
class Pump(MercuryChannel, Writable):
|
||||||
kind = 'PUMP'
|
kind = 'PUMP'
|
||||||
value = Parameter('pump state', EnumType(off=0, on=1))
|
value = Parameter('pump state', EnumType(off=0, on=1))
|
||||||
target = Parameter('pump target', EnumType(off=0, on=1))
|
target = FrozenParam('pump target', EnumType(off=0, on=1))
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.query('DEV::PUMP:SIG:STATE', off_on)
|
return self.query('DEV::PUMP:SIG:STATE', off_on)
|
||||||
|
|
||||||
|
# as target is a FrozenParam, value might be still lag behind target
|
||||||
|
# but will be updated when changed from an other source
|
||||||
|
read_target = read_value
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
return self.change('DEV::PUMP:SIG:STATE', value, off_on)
|
self.change('DEV::PUMP:SIG:STATE', value, off_on)
|
||||||
|
return value
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
return IDLE, ''
|
return IDLE, ''
|
||||||
|
@ -411,6 +411,7 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
|
|
||||||
@Command()
|
@Command()
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop motor and control"""
|
||||||
if self.motor.isBusy():
|
if self.motor.isBusy():
|
||||||
self.log.info('stop motor')
|
self.log.info('stop motor')
|
||||||
self.motor_stop()
|
self.motor_stop()
|
||||||
|
155
test/test_callbacks.py
Normal file
155
test/test_callbacks.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""test parameter callbacks"""
|
||||||
|
|
||||||
|
from test.test_modules import LoggerStub, ServerStub
|
||||||
|
import pytest
|
||||||
|
from frappy.core import Module, Parameter, FloatRange
|
||||||
|
from frappy.errors import WrongTypeError
|
||||||
|
|
||||||
|
|
||||||
|
WRONG_TYPE = WrongTypeError()
|
||||||
|
|
||||||
|
|
||||||
|
class Mod(Module):
|
||||||
|
a = Parameter('', FloatRange())
|
||||||
|
b = Parameter('', FloatRange())
|
||||||
|
c = Parameter('', FloatRange())
|
||||||
|
|
||||||
|
def read_a(self):
|
||||||
|
raise WRONG_TYPE
|
||||||
|
|
||||||
|
def read_b(self):
|
||||||
|
raise WRONG_TYPE
|
||||||
|
|
||||||
|
def read_c(self):
|
||||||
|
raise WRONG_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
class Dbl(Module):
|
||||||
|
a = Parameter('', FloatRange())
|
||||||
|
b = Parameter('', FloatRange())
|
||||||
|
c = Parameter('', FloatRange())
|
||||||
|
_error_a = None
|
||||||
|
_value_b = None
|
||||||
|
_error_c = None
|
||||||
|
|
||||||
|
def update_a(self, value, err=None):
|
||||||
|
# treat error updates
|
||||||
|
try:
|
||||||
|
self.a = value * 2
|
||||||
|
except TypeError: # value is None -> err
|
||||||
|
self.announceUpdate('a', None, err)
|
||||||
|
|
||||||
|
def update_b(self, value):
|
||||||
|
self._value_b = value
|
||||||
|
# error updates are ignored
|
||||||
|
self.b = value * 2
|
||||||
|
|
||||||
|
|
||||||
|
def make(cls):
|
||||||
|
logger = LoggerStub()
|
||||||
|
srv = ServerStub({})
|
||||||
|
return cls('mod1', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_callback():
|
||||||
|
mod1 = make(Mod)
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def cbfunc(arg1, arg2, value):
|
||||||
|
result[:] = arg1, arg2, value
|
||||||
|
|
||||||
|
mod1.addCallback('a', cbfunc, 'ARG1', 'arg2')
|
||||||
|
|
||||||
|
mod1.a = 1.5
|
||||||
|
assert result == ['ARG1', 'arg2', 1.5]
|
||||||
|
|
||||||
|
result.clear()
|
||||||
|
|
||||||
|
with pytest.raises(WrongTypeError):
|
||||||
|
mod1.read_a()
|
||||||
|
|
||||||
|
assert not result # callback function is NOT called
|
||||||
|
|
||||||
|
|
||||||
|
def test_combi_callback():
|
||||||
|
mod1 = make(Mod)
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def cbfunc(arg1, arg2, value, err=None):
|
||||||
|
result[:] = arg1, arg2, value, err
|
||||||
|
|
||||||
|
mod1.addCallback('a', cbfunc, 'ARG1', 'arg2')
|
||||||
|
|
||||||
|
mod1.a = 1.5
|
||||||
|
assert result == ['ARG1', 'arg2', 1.5, None]
|
||||||
|
|
||||||
|
result.clear()
|
||||||
|
|
||||||
|
with pytest.raises(WrongTypeError):
|
||||||
|
mod1.read_a()
|
||||||
|
|
||||||
|
assert result[:3] == ['ARG1', 'arg2', None] # callback function called with value None
|
||||||
|
assert isinstance(result[3], WrongTypeError)
|
||||||
|
|
||||||
|
|
||||||
|
def test_autoupdate():
|
||||||
|
mod1 = make(Mod)
|
||||||
|
mod2 = make(Dbl)
|
||||||
|
mod1.registerCallbacks(mod2, autoupdate=['c'])
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
def cbfunc(pname, *args):
|
||||||
|
result[pname] = args
|
||||||
|
|
||||||
|
for param in 'a', 'b', 'c':
|
||||||
|
mod2.addCallback(param, cbfunc, param)
|
||||||
|
|
||||||
|
# test update_a without error
|
||||||
|
mod1.a = 5
|
||||||
|
assert mod2.a == 10
|
||||||
|
assert result.pop('a') == (10,)
|
||||||
|
|
||||||
|
# test update_a with error
|
||||||
|
with pytest.raises(WrongTypeError):
|
||||||
|
mod1.read_a()
|
||||||
|
|
||||||
|
assert result.pop('a') == (None, WRONG_TYPE)
|
||||||
|
|
||||||
|
# test that update_b is ignored in case of error
|
||||||
|
mod1.b = 3
|
||||||
|
assert mod2.b == 6 # no error
|
||||||
|
assert result.pop('b') == (6,)
|
||||||
|
|
||||||
|
with pytest.raises(WrongTypeError):
|
||||||
|
mod1.read_b()
|
||||||
|
assert 'b' not in result
|
||||||
|
|
||||||
|
# test autoupdate
|
||||||
|
mod1.c = 3
|
||||||
|
assert mod2.c == 3
|
||||||
|
assert result['c'] == (3,)
|
||||||
|
|
||||||
|
with pytest.raises(WrongTypeError):
|
||||||
|
mod1.read_c()
|
||||||
|
assert result['c'] == (None, WRONG_TYPE)
|
@ -18,12 +18,14 @@
|
|||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""test frappy.mixins.HasCtrlPars"""
|
"""test frappy.extparams"""
|
||||||
|
|
||||||
|
|
||||||
from test.test_modules import LoggerStub, ServerStub
|
from test.test_modules import LoggerStub, ServerStub
|
||||||
|
import pytest
|
||||||
from frappy.core import FloatRange, Module, Parameter
|
from frappy.core import FloatRange, Module, Parameter
|
||||||
from frappy.structparam import StructParam
|
from frappy.extparams import StructParam, FloatEnumParam
|
||||||
|
from frappy.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
def test_with_read_ctrlpars():
|
def test_with_read_ctrlpars():
|
||||||
@ -130,3 +132,76 @@ def test_order_dependence1():
|
|||||||
def test_order_dependence2():
|
def test_order_dependence2():
|
||||||
test_with_read_ctrlpars()
|
test_with_read_ctrlpars()
|
||||||
test_without_read_ctrlpars()
|
test_without_read_ctrlpars()
|
||||||
|
|
||||||
|
|
||||||
|
def test_float_enum():
|
||||||
|
class Mod(Module):
|
||||||
|
vrange = FloatEnumParam('voltage range', [
|
||||||
|
(1, '50uV'), '200 µV', '1mV', ('5mV', 0.006), (9, 'max', 0.024)], 'V')
|
||||||
|
gain = FloatEnumParam('gain factor', ('1', '2', '4', '8'), idx_name='igain')
|
||||||
|
dist = FloatEnumParam('distance', ('1m', '1mm', '1µm'), unit='m')
|
||||||
|
|
||||||
|
_vrange_idx = None
|
||||||
|
|
||||||
|
def write_vrange_idx(self, value):
|
||||||
|
self._vrange_idx = value
|
||||||
|
|
||||||
|
logger = LoggerStub()
|
||||||
|
updates = {}
|
||||||
|
srv = ServerStub(updates)
|
||||||
|
|
||||||
|
m = Mod('m', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
assert m.write_vrange_idx(1) == 1
|
||||||
|
assert m._vrange_idx == '50uV'
|
||||||
|
assert m._vrange_idx == 1
|
||||||
|
assert m.vrange == 5e-5
|
||||||
|
|
||||||
|
assert m.write_vrange_idx(2) == 2
|
||||||
|
assert m._vrange_idx == '200 µV'
|
||||||
|
assert m._vrange_idx == 2
|
||||||
|
assert m.vrange == 2e-4
|
||||||
|
|
||||||
|
assert m.write_vrange(6e-5) == 5e-5 # round to the next value
|
||||||
|
assert m._vrange_idx == '50uV'
|
||||||
|
assert m._vrange_idx == 1
|
||||||
|
assert m.write_vrange(20e-3) == 24e-3 # round to the next value
|
||||||
|
assert m._vrange_idx == 'max'
|
||||||
|
assert m._vrange_idx == 9
|
||||||
|
|
||||||
|
for idx in range(4):
|
||||||
|
value = 2 ** idx
|
||||||
|
updates.clear()
|
||||||
|
assert m.write_igain(idx) == idx
|
||||||
|
assert updates == {'m': {'igain': idx, 'gain': value}}
|
||||||
|
assert m.igain == idx
|
||||||
|
assert m.igain == str(value)
|
||||||
|
assert m.gain == value
|
||||||
|
|
||||||
|
for idx in range(4):
|
||||||
|
value = 2 ** idx
|
||||||
|
assert m.write_gain(value) == value
|
||||||
|
assert m.igain == idx
|
||||||
|
assert m.igain == str(value)
|
||||||
|
|
||||||
|
for idx in range(3):
|
||||||
|
value = 10 ** (-3 * idx)
|
||||||
|
assert m.write_dist(value) == value
|
||||||
|
assert m.dist_idx == idx
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('labels, unit, error', [
|
||||||
|
(FloatRange(), '', 'not a datatype'), # 2nd arg must not be a datatype
|
||||||
|
([(1, 2, 3)], '', 'must be strings'), # label is not a string
|
||||||
|
([(1, '1V', 3, 4)], 'V', 'labels or tuples'), # 4-tuple
|
||||||
|
([('1A', 3, 4)], 'A', 'labels or tuples'), # two values after label
|
||||||
|
(('1m', (0, '1k')), '', 'conflicts with'), # two times index 0
|
||||||
|
(['1mV', '10mA'], 'V', 'not the form'), # wrong unit
|
||||||
|
(['.mV'], 'V', 'not the form'), # bad number
|
||||||
|
(['mV'], 'V', 'not the form'), # missing number
|
||||||
|
(['1+mV'], 'V', 'not the form'), # bad number
|
||||||
|
])
|
||||||
|
def test_bad_float_enum(labels, unit, error):
|
||||||
|
with pytest.raises(ProgrammingError, match=error):
|
||||||
|
class Mod(Module): # pylint:disable=unused-variable
|
||||||
|
param = FloatEnumParam('', labels, unit)
|
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import importlib
|
||||||
|
from glob import glob
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||||
@ -440,12 +442,12 @@ def test_override():
|
|||||||
assert Mod.value.value == 5
|
assert Mod.value.value == 5
|
||||||
assert Mod.stop.description == "no decorator needed"
|
assert Mod.stop.description == "no decorator needed"
|
||||||
|
|
||||||
class Mod2(Drivable):
|
class Mod2(Mod):
|
||||||
@Command()
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert Mod2.stop.description == Drivable.stop.description
|
# inherit doc string
|
||||||
|
assert Mod2.stop.description == Mod.stop.description
|
||||||
|
|
||||||
|
|
||||||
def test_command_config():
|
def test_command_config():
|
||||||
@ -920,3 +922,24 @@ def test_interface_classes(bases, iface_classes):
|
|||||||
pass
|
pass
|
||||||
m = Mod('mod', LoggerStub(), {'description': 'test'}, srv)
|
m = Mod('mod', LoggerStub(), {'description': 'test'}, srv)
|
||||||
assert m.interface_classes == iface_classes
|
assert m.interface_classes == iface_classes
|
||||||
|
|
||||||
|
|
||||||
|
all_drivables = set()
|
||||||
|
for pyfile in glob('frappy_*/*.py'):
|
||||||
|
module = pyfile[:-3].replace('/', '.')
|
||||||
|
try:
|
||||||
|
importlib.import_module(module)
|
||||||
|
except Exception as e:
|
||||||
|
print(module, e)
|
||||||
|
continue
|
||||||
|
for obj_ in sys.modules[module].__dict__.values():
|
||||||
|
if isinstance(obj_, type) and issubclass(obj_, Drivable):
|
||||||
|
all_drivables.add(obj_)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('modcls', all_drivables)
|
||||||
|
def test_stop_doc(modcls):
|
||||||
|
# make sure that implemented stop methods have a doc string
|
||||||
|
if (modcls.stop.description == Drivable.stop.description
|
||||||
|
and modcls.stop.func != Drivable.stop.func):
|
||||||
|
assert modcls.stop.func.__doc__ # stop method needs a doc string
|
||||||
|
Reference in New Issue
Block a user