Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 298d43469a | |||
| f563999a9e | |||
| 1ea8aad20c | |||
| aa753b8e7e | |||
| 71ab0bebd5 | |||
| 18f6fa239b | |||
| 00318cc7a1 | |||
| 7167d02613 | |||
| e1e642fb2f | |||
| 53256d1583 | |||
| e741404d0b | |||
| d0b56ae918 | |||
| c353ed3499 | |||
| e616b40fc8 | |||
| 40934e45bc | |||
| ce29430e18 | |||
| 9d98a381b0 | |||
| 32dad35075 | |||
| 705f0173f4 | |||
| 3cb6b10183 | |||
| d7a07b63ae | |||
| 07263281fd | |||
| 600d11d3bb | |||
| 8f835e3d3d | |||
| ec226a9124 | |||
| 7d0ca5f9dd | |||
| 6ea8bc6e52 | |||
| 75c3161035 | |||
| ecf4192d53 | |||
| 3586f53c3d | |||
| 7994177873 | |||
| 8e95fa9266 |
@@ -86,7 +86,7 @@ dummy-variables-rgx=_|dummy
|
|||||||
|
|
||||||
# List of additional names supposed to be defined in builtins. Remember that
|
# List of additional names supposed to be defined in builtins. Remember that
|
||||||
# you should avoid to define new builtins when possible.
|
# you should avoid to define new builtins when possible.
|
||||||
additional-builtins=Node,Mod,Param,Command,Group
|
additional-builtins=Node,Mod,Param,Command,Group,IO
|
||||||
|
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
|
|||||||
45
bin/frappy-edit
Executable file
45
bin/frappy-edit
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# *****************************************************************************
|
||||||
|
# Copyright (c) 2015-2024 by the authors, see LICENSE
|
||||||
|
#
|
||||||
|
# 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 sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add import path for inplace usage
|
||||||
|
repo = Path(__file__).absolute().parents[1]
|
||||||
|
sys.path.insert(0, str(repo))
|
||||||
|
|
||||||
|
from frappy.lib import generalConfig
|
||||||
|
from frappy.tools.cfgedit import EditorMain
|
||||||
|
|
||||||
|
# merge cfg dirs from env variable and the ones typically used at psi
|
||||||
|
# use dicts instead of sets, as we want to keep order
|
||||||
|
cfgdirs = os.environ.get('FRAPPY_CONFDIR', None)
|
||||||
|
cfgdirs = cfgdirs.split(':') if cfgdirs else []
|
||||||
|
for cfgdir in 'cfg', 'cfg/main', 'cfg/stick', 'cfg/addons':
|
||||||
|
cfgpath = repo / cfgdir
|
||||||
|
if cfgpath.exists():
|
||||||
|
cfgdirs.append(str(cfgpath))
|
||||||
|
os.environ['FRAPPY_CONFDIR'] = ':'.join(cfgdirs)
|
||||||
|
generalConfig.init()
|
||||||
|
EditorMain(sys.argv[1]).run()
|
||||||
22
cfg/addons/laser_cfg.py
Normal file
22
cfg/addons/laser_cfg.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Node('pdld_laser.psi.ch',
|
||||||
|
'PDLD laser',
|
||||||
|
interface = 'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('laser_io',
|
||||||
|
'frappy_psi.pdld.IO',
|
||||||
|
'laser IO',
|
||||||
|
uri='serial:///dev/ttyUSB0?baudrate=9600',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('laser',
|
||||||
|
'frappy_psi.pdld.Laser',
|
||||||
|
'laser switch',
|
||||||
|
io='laser_io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('laser_power',
|
||||||
|
'frappy_psi.pdld.LaserPower',
|
||||||
|
'laser power',
|
||||||
|
io='laser_io',
|
||||||
|
)
|
||||||
@@ -5,7 +5,7 @@ Node('stickmotor.linse.psi.ch',
|
|||||||
Mod('stick_io',
|
Mod('stick_io',
|
||||||
'frappy_psi.phytron.PhytronIO',
|
'frappy_psi.phytron.PhytronIO',
|
||||||
'dom motor IO',
|
'dom motor IO',
|
||||||
uri='ldmcc08-ts:3006',
|
uri='ldmcc05-ts:3006',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('stickrot',
|
Mod('stickrot',
|
||||||
|
|||||||
19
cfg/bronkhorst_cfg.py
Normal file
19
cfg/bronkhorst_cfg.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Node('bronkhorsttest.psi.ch',
|
||||||
|
'bronkhorst test',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('io',
|
||||||
|
'frappy_psi.bronkhorst.IO',
|
||||||
|
'bronkhorst communication',
|
||||||
|
uri='tcp://localhost:3005',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('p',
|
||||||
|
'frappy_psi.bronkhorst.Controller',
|
||||||
|
'pressure controller',
|
||||||
|
io='io',
|
||||||
|
adr=128,
|
||||||
|
scale=18,
|
||||||
|
value=Param(unit='mbar')
|
||||||
|
)
|
||||||
17
cfg/bronkhorst_test_cfg.py
Normal file
17
cfg/bronkhorst_test_cfg.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Node('bronkhorst_test.psi.ch',
|
||||||
|
'bronkhorst test',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('io',
|
||||||
|
'frappy_psi.bronkhorst.IO',
|
||||||
|
'',
|
||||||
|
uri='dil4-ts:3002',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('flow',
|
||||||
|
'frappy_psi.bronkhorst.Sensor',
|
||||||
|
'flow',
|
||||||
|
io='io',
|
||||||
|
value=Param(unit='ln/min'),
|
||||||
|
)
|
||||||
100
cfg/dil4_test_cfg.py
Normal file
100
cfg/dil4_test_cfg.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
Node('dil4_test.psi.ch',
|
||||||
|
'dil4 test',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('io',
|
||||||
|
'frappy_psi.oiclassic.IGH_IO',
|
||||||
|
'',
|
||||||
|
uri='dil4-ts:3005',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('p',
|
||||||
|
'frappy_psi.oiclassic.Pressure',
|
||||||
|
'pressure',
|
||||||
|
io='io',
|
||||||
|
addr='G2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('P_mix',
|
||||||
|
'frappy_psi.oiclassic.MixPower',
|
||||||
|
'mix power',
|
||||||
|
io='io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('P_sorb',
|
||||||
|
'frappy_psi.oiclassic.SorbPower',
|
||||||
|
'sorb power',
|
||||||
|
io='io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('P_still',
|
||||||
|
'frappy_psi.oiclassic.StillPower',
|
||||||
|
'still power',
|
||||||
|
io='io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('mot_fast',
|
||||||
|
'frappy_psi.oiclassic.MotorValve',
|
||||||
|
'fast motor valve',
|
||||||
|
io='io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('mot_slow',
|
||||||
|
'frappy_psi.oiclassic.SlowMotorValve',
|
||||||
|
'slow motor valve',
|
||||||
|
io='io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('valve',
|
||||||
|
'frappy_psi.oiclassic.Valve',
|
||||||
|
'solenoid valve',
|
||||||
|
io='io',
|
||||||
|
addr='V2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('valve_pulsed',
|
||||||
|
'frappy_psi.oiclassic.PulsedValve',
|
||||||
|
'pulsed valve',
|
||||||
|
io='io',
|
||||||
|
addr='V2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('upperLN2',
|
||||||
|
'frappy_psi.oiclassic.N2Sensor',
|
||||||
|
'upper LN2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('lowerLN2',
|
||||||
|
'frappy_psi.oiclassic.N2Sensor',
|
||||||
|
'lower LN2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('pump',
|
||||||
|
'frappy_psi.oiclassic.Pump',
|
||||||
|
'pump feedback',
|
||||||
|
io='io',
|
||||||
|
addr='rotary_pump_He3',
|
||||||
|
upper_LN2='upperLN2',
|
||||||
|
lower_LN2='lowerLN2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('io_flow',
|
||||||
|
'frappy_psi.bronkhorst.IO',
|
||||||
|
'',
|
||||||
|
uri='dil4-ts:3002',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('flow',
|
||||||
|
'frappy_psi.bronkhorst.Sensor',
|
||||||
|
'flow',
|
||||||
|
io='io_flow',
|
||||||
|
value=Param(unit='ln/min'),
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('fun',
|
||||||
|
'frappy_psi.softcal.Function',
|
||||||
|
'modified flow',
|
||||||
|
rawsensor='flow',
|
||||||
|
formula='x',
|
||||||
|
)
|
||||||
@@ -83,37 +83,38 @@ Mod('compressor',
|
|||||||
)
|
)
|
||||||
|
|
||||||
Mod('p2',
|
Mod('p2',
|
||||||
'frappy_psi.logo.Pressure',
|
'frappy_psi.logo.Value',
|
||||||
'pressure after compressor',
|
'pressure after compressor',
|
||||||
io = 'logo',
|
io = 'logo',
|
||||||
addr ="VW0",
|
addr ="VW0",
|
||||||
pollinterval=0.5,
|
value = Param(unit='mbar'),
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('p1',
|
Mod('p1',
|
||||||
'frappy_psi.logo.Pressure',
|
'frappy_psi.logo.Value',
|
||||||
'dump pressure',
|
'dump pressure',
|
||||||
io = 'logo',
|
io = 'logo',
|
||||||
addr ="VW28",
|
addr ="VW28",
|
||||||
pollinterval=0.5,
|
value = Param(unit='mbar'),
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('p5',
|
Mod('p5',
|
||||||
'frappy_psi.logo.Pressure',
|
'frappy_psi.logo.Value',
|
||||||
'pressure after forepump',
|
'pressure after forepump',
|
||||||
io = 'logo',
|
io = 'logo',
|
||||||
addr ="VW4",
|
addr ="VW4",
|
||||||
pollinterval = 0.5,
|
value = Param(unit='mbar'),
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('airpressure',
|
Mod('airpressure',
|
||||||
'frappy_psi.logo.Comparator',
|
'frappy_psi.logo.DigitalValue',
|
||||||
'Airpressure state',
|
'Airpressure state',
|
||||||
io = 'logo',
|
io = 'logo',
|
||||||
addr ="V1024.7",
|
addr ="V1024.7",
|
||||||
threshold = 500,
|
)
|
||||||
pollinterval = 0.5,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Mod('io_pfeiffer',
|
Mod('io_pfeiffer',
|
||||||
'frappy_psi.pfeiffer_new.PfeifferProtocol',
|
'frappy_psi.pfeiffer_new.PfeifferProtocol',
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
Node('gas10ka.psi.ch',
|
|
||||||
'10kBar Gas pressure stick',
|
|
||||||
interface='tcp://5010',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('io',
|
|
||||||
'frappy_psi.logo.IO',
|
|
||||||
'',
|
|
||||||
ip_address = "192.168.1.1",
|
|
||||||
tcap_client = 0x3000,
|
|
||||||
tsap_server = 0x2000
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('R_pt10k',
|
|
||||||
'frappy_psi.logo.Resistor',
|
|
||||||
'raw sensor value of T_p10k',
|
|
||||||
io = 'io',
|
|
||||||
addr = "VW0",
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('T_pt10k',
|
|
||||||
'frappy_psi.softcal.Sensor',
|
|
||||||
'temperature close to sample',
|
|
||||||
value=Param(unit='K'),
|
|
||||||
rawsensor='R_pt10k',
|
|
||||||
calcurve='pt10000e',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('R_top',
|
|
||||||
'frappy_psi.logo.Resistor',
|
|
||||||
'raw sensor value of T_top',
|
|
||||||
io = 'io',
|
|
||||||
addr = "VW2",
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('T_top',
|
|
||||||
'frappy_psi.softcal.Sensor',
|
|
||||||
'capillary temperature at highest position',
|
|
||||||
value=Param(unit='K'),
|
|
||||||
rawsensor='R_top',
|
|
||||||
calcurve='pt1000e',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('R_mid',
|
|
||||||
'frappy_psi.logo.Resistor',
|
|
||||||
'raw sensor value of T_mid',
|
|
||||||
io = 'io',
|
|
||||||
addr = "VW6",
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('T_mid',
|
|
||||||
'frappy_psi.softcal.Sensor',
|
|
||||||
'capillary temperature at mid position',
|
|
||||||
value=Param(unit='K'),
|
|
||||||
rawsensor='R_mid',
|
|
||||||
calcurve='pt1000e',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('R_bot',
|
|
||||||
'frappy_psi.logo.Resistor',
|
|
||||||
'raw sensor value of T_bot',
|
|
||||||
io = 'io',
|
|
||||||
addr = "VW4",
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('T_bot',
|
|
||||||
'frappy_psi.softcal.Sensor',
|
|
||||||
'capillary temperature at lower position',
|
|
||||||
value=Param(unit='K'),
|
|
||||||
rawsensor='R_bot',
|
|
||||||
calcurve='pt1000e',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Mod('R_sam_cx',
|
|
||||||
'frappy_psi.logo.Resistor',
|
|
||||||
'sensor',
|
|
||||||
io = 'io',
|
|
||||||
addr = "VW16",
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('T_sam_cx',
|
|
||||||
'frappy_psi.softcal.Sensor',
|
|
||||||
'?',
|
|
||||||
value=Param(unit='K'),
|
|
||||||
rawsensor='R_sam_cx',
|
|
||||||
calcurve='X174785',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('heater',
|
|
||||||
'frappy_psi.capillary_heater.Heater',
|
|
||||||
'the capillary heater',
|
|
||||||
io = 'io',
|
|
||||||
)
|
|
||||||
16
cfg/hcp_cfg.py
Normal file
16
cfg/hcp_cfg.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Node('hcptest.psi.ch',
|
||||||
|
'high voltage supply test',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('io',
|
||||||
|
'frappy_psi.hcp.IO',
|
||||||
|
'hcp communication',
|
||||||
|
uri='serial:///dev/tty.usbserial-21440?baudrate=9600',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('voltage',
|
||||||
|
'frappy_psi.hcp.Voltage',
|
||||||
|
'fug hcp 14-6500 voltage',
|
||||||
|
io='io',
|
||||||
|
)
|
||||||
@@ -6,11 +6,7 @@ lakeshore_uri = environ.get('LS_URI', 'tcp://<host>:7777')
|
|||||||
Node('example_cryo.psi.ch', # a globally unique identification
|
Node('example_cryo.psi.ch', # a globally unique identification
|
||||||
'this is an example cryostat for the Frappy tutorial', # describes the node
|
'this is an example cryostat for the Frappy tutorial', # describes the node
|
||||||
interface='tcp://10767') # you might choose any port number > 1024
|
interface='tcp://10767') # you might choose any port number > 1024
|
||||||
Mod('io', # the name of the module
|
IO('io', lakeshore_uri) # the communicator (its class will be detected automatically)
|
||||||
'frappy_demo.lakeshore.LakeshoreIO', # the class used for communication
|
|
||||||
'communication to main controller', # a description
|
|
||||||
uri=lakeshore_uri, # the serial connection
|
|
||||||
)
|
|
||||||
Mod('T',
|
Mod('T',
|
||||||
'frappy_demo.lakeshore.TemperatureLoop',
|
'frappy_demo.lakeshore.TemperatureLoop',
|
||||||
'Sample Temperature',
|
'Sample Temperature',
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ Mod('ts',
|
|||||||
'frappy_psi.parmod.Converging',
|
'frappy_psi.parmod.Converging',
|
||||||
'test for parmod',
|
'test for parmod',
|
||||||
unit='K',
|
unit='K',
|
||||||
value_param='th.value',
|
read='th.value',
|
||||||
target_param='th.setsamp',
|
write='th.setsamp',
|
||||||
meaning=['temperature', 20],
|
meaning=['temperature', 20],
|
||||||
settling_time=20,
|
settling_time=20,
|
||||||
tolerance=1,
|
tolerance=1,
|
||||||
|
|||||||
@@ -1,211 +1,195 @@
|
|||||||
Node('mb11.psi.ch',
|
# please edit this file with frappy edit
|
||||||
'MB11 11 Tesla - 100 mm cryomagnet',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('itc1',
|
doc="""MB11 11 Tesla - 100 mm cryomagnet
|
||||||
'frappy_psi.mercury.IO',
|
|
||||||
'ITC for heat exchanger and pressures',
|
|
||||||
uri='mb11-ts:3001',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('itc2',
|
after conversion with frappy edit
|
||||||
'frappy_psi.mercury.IO',
|
"""
|
||||||
'ITC for neck and nv heaters',
|
Node('mb11.psi.ch', doc,
|
||||||
uri='mb11-ts:3002',
|
interface='tcp://10767',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('ips',
|
IO('itc1', 'mb11-ts:3001')
|
||||||
'frappy_psi.mercury.IO',
|
|
||||||
'IPS for magnet and levels',
|
IO('itc2', 'mb11-ts:3002')
|
||||||
uri='mb11-ts:3003',
|
|
||||||
)
|
IO('ips', 'mb11-ts:3003')
|
||||||
|
|
||||||
Mod('T_stat',
|
Mod('T_stat',
|
||||||
'frappy_psi.mercury.TemperatureAutoFlow',
|
'frappy_psi.mercury.TemperatureAutoFlow',
|
||||||
'static heat exchanger temperature',
|
'static heat exchanger temperature',
|
||||||
meaning=['temperature_regulation', 27],
|
meaning = ('temperature_regulation', 27),
|
||||||
output_module='htr_stat',
|
output_module = 'htr_stat',
|
||||||
needle_valve='p_stat',
|
needle_valve = 'p_stat',
|
||||||
slot='DB6.T1',
|
slot = 'DB6.T1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
tolerance=0.1,
|
tolerance = 0.1,
|
||||||
flowpars=((1,5), (2, 20)),
|
flowpars = ((1, 5), (2, 20)),
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_stat',
|
Mod('htr_stat',
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
'frappy_psi.mercury.HeaterOutput',
|
||||||
'static heat exchanger heater',
|
'static heat exchanger heater',
|
||||||
slot='DB1.H1',
|
slot = 'DB1.H1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('p_stat',
|
Mod('p_stat',
|
||||||
'frappy_psi.mercury.PressureLoop',
|
'frappy_psi.mercury.PressureLoop',
|
||||||
'static needle valve pressure',
|
'static needle valve pressure',
|
||||||
output_module='pos_stat',
|
output_module = 'pos_stat',
|
||||||
settling_time=60.0,
|
settling_time = 60,
|
||||||
slot='DB5.P1',
|
slot = 'DB5.P1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
tolerance=1.0,
|
tolerance = 1,
|
||||||
value=Param(
|
value = Param(unit='mbar_flow'),
|
||||||
unit='mbar_flow',
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('pos_stat',
|
Mod('pos_stat',
|
||||||
'frappy_psi.mercury.ValvePos',
|
'frappy_psi.mercury.ValvePos',
|
||||||
'static needle valve position',
|
'static needle valve position',
|
||||||
slot='DB5.P1,DB3.G1',
|
slot = 'DB5.P1,DB3.G1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T_dyn',
|
Mod('T_dyn',
|
||||||
'frappy_psi.mercury.TemperatureAutoFlow',
|
'frappy_psi.mercury.TemperatureAutoFlow',
|
||||||
'dynamic heat exchanger temperature',
|
'dynamic heat exchanger temperature',
|
||||||
output_module='htr_dyn',
|
output_module = 'htr_dyn',
|
||||||
needle_valve='p_dyn',
|
needle_valve = 'p_dyn',
|
||||||
slot='DB7.T1',
|
slot = 'DB7.T1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
tolerance=0.1,
|
tolerance = 0.1,
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_dyn',
|
Mod('htr_dyn',
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
'frappy_psi.mercury.HeaterOutput',
|
||||||
'dynamic heat exchanger heater',
|
'dynamic heat exchanger heater',
|
||||||
slot='DB2.H1',
|
slot = 'DB2.H1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('p_dyn',
|
Mod('p_dyn',
|
||||||
'frappy_psi.mercury.PressureLoop',
|
'frappy_psi.mercury.PressureLoop',
|
||||||
'dynamic needle valve pressure',
|
'dynamic needle valve pressure',
|
||||||
output_module='pos_dyn',
|
output_module = 'pos_dyn',
|
||||||
settling_time=60.0,
|
settling_time = 60,
|
||||||
slot='DB8.P1',
|
slot = 'DB8.P1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
tolerance=1.0,
|
tolerance = 1,
|
||||||
value=Param(
|
value = Param(unit='mbar_flow'),
|
||||||
unit='mbar_flow',
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('pos_dyn',
|
Mod('pos_dyn',
|
||||||
'frappy_psi.mercury.ValvePos',
|
'frappy_psi.mercury.ValvePos',
|
||||||
'dynamic needle valve position',
|
'dynamic needle valve position',
|
||||||
slot='DB8.P1,DB4.G1',
|
slot = 'DB8.P1,DB4.G1',
|
||||||
io='itc1',
|
io = 'itc1',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('mf',
|
Mod('mf',
|
||||||
'frappy_psi.ips_mercury.Field',
|
'frappy_psi.ips_mercury.Field',
|
||||||
'magnetic field',
|
'magnetic field',
|
||||||
slot='GRPZ',
|
slot = 'GRPZ',
|
||||||
io='ips',
|
io = 'ips',
|
||||||
tolerance=0.001,
|
tolerance = 0.001,
|
||||||
wait_stable_field=60.0,
|
wait_stable_field = 60,
|
||||||
target=Param(
|
target = Param(max=11),
|
||||||
max=11.0,
|
persistent_limit = 11.1,
|
||||||
),
|
)
|
||||||
persistent_limit=11.1,
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('lev',
|
Mod('lev',
|
||||||
'frappy_psi.mercury.HeLevel',
|
'frappy_psi.mercury.HeLevel',
|
||||||
'LHe level',
|
'LHe level',
|
||||||
slot='DB1.L1',
|
slot = 'DB1.L1',
|
||||||
io='ips',
|
io = 'ips',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('n2lev',
|
Mod('n2lev',
|
||||||
'frappy_psi.mercury.N2Level',
|
'frappy_psi.mercury.N2Level',
|
||||||
'LN2 level',
|
'LN2 level',
|
||||||
slot='DB1.L1',
|
slot = 'DB1.L1',
|
||||||
io='ips',
|
io = 'ips',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T_neck1',
|
Mod('T_neck1',
|
||||||
'frappy_psi.mercury.TemperatureLoop',
|
'frappy_psi.mercury.TemperatureLoop',
|
||||||
'neck heater 1 temperature',
|
'neck heater 1 temperature',
|
||||||
output_module='htr_neck1',
|
output_module = 'htr_neck1',
|
||||||
slot='MB1.T1',
|
slot = 'MB1.T1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
tolerance=1.0,
|
tolerance = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_neck1',
|
Mod('htr_neck1',
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
'frappy_psi.mercury.HeaterOutput',
|
||||||
'neck heater 1 power',
|
'neck heater 1 power',
|
||||||
slot='MB0.H1',
|
slot = 'MB0.H1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T_neck2',
|
Mod('T_neck2',
|
||||||
'frappy_psi.mercury.TemperatureLoop',
|
'frappy_psi.mercury.TemperatureLoop',
|
||||||
'neck heater 2 temperature',
|
'neck heater 2 temperature',
|
||||||
output_module='htr_neck2',
|
output_module = 'htr_neck2',
|
||||||
slot='DB6.T1',
|
slot = 'DB6.T1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
tolerance=1.0,
|
tolerance = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_neck2',
|
Mod('htr_neck2',
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
'frappy_psi.mercury.HeaterOutput',
|
||||||
'neck heater 2 power',
|
'neck heater 2 power',
|
||||||
slot='DB1.H1',
|
slot = 'DB1.H1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T_nvs',
|
Mod('T_nvs',
|
||||||
'frappy_psi.mercury.TemperatureLoop',
|
'frappy_psi.mercury.TemperatureLoop',
|
||||||
'static needle valve temperature',
|
'static needle valve temperature',
|
||||||
output_module='htr_nvs',
|
output_module = 'htr_nvs',
|
||||||
slot='DB7.T1',
|
slot = 'DB7.T1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
tolerance=0.1,
|
tolerance = 0.1,
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_nvs',
|
Mod('htr_nvs',
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
'frappy_psi.mercury.HeaterOutput',
|
||||||
'static needle valve heater power',
|
'static needle valve heater power',
|
||||||
slot='DB2.H1',
|
slot = 'DB2.H1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T_nvd',
|
Mod('T_nvd',
|
||||||
'frappy_psi.mercury.TemperatureLoop',
|
'frappy_psi.mercury.TemperatureLoop',
|
||||||
'dynamic needle valve heater temperature',
|
'dynamic needle valve heater temperature',
|
||||||
output_module='htr_nvd',
|
output_module = 'htr_nvd',
|
||||||
slot='DB8.T1',
|
slot = 'DB8.T1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
tolerance=0.1,
|
tolerance = 0.1,
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('htr_nvd',
|
Mod('htr_nvd',
|
||||||
'frappy_psi.mercury.HeaterOutput',
|
'frappy_psi.mercury.HeaterOutput',
|
||||||
'dynamic needle valve heater power',
|
'dynamic needle valve heater power',
|
||||||
slot='DB3.H1',
|
slot = 'DB3.H1',
|
||||||
io='itc2',
|
io = 'itc2',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('T_coil',
|
Mod('T_coil',
|
||||||
'frappy_psi.mercury.TemperatureSensor',
|
'frappy_psi.mercury.TemperatureSensor',
|
||||||
'coil temperature',
|
'coil temperature',
|
||||||
slot='MB1.T1',
|
slot = 'MB1.T1',
|
||||||
io='ips',
|
io = 'ips',
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('om_io',
|
IO('om_io', 'mb11-ts.psi.ch:3004')
|
||||||
'frappy_psi.phytron.PhytronIO',
|
|
||||||
'dom motor IO',
|
|
||||||
uri='mb11-ts.psi.ch:3004',
|
|
||||||
)
|
|
||||||
|
|
||||||
Mod('om',
|
Mod('om',
|
||||||
'frappy_psi.phytron.Motor',
|
'frappy_psi.phytron.Motor',
|
||||||
'stick rotation, typically used for omega',
|
'stick rotation, typically used for omega',
|
||||||
io='om_io',
|
io = 'om_io',
|
||||||
target_min=-360,
|
target_min = '-360',
|
||||||
target_max=360,
|
target_max = '360',
|
||||||
encoder_mode='NO',
|
encoder_mode = 'NO',
|
||||||
target=Param(min=-360, max=360),
|
target = Param(min=-360, max=360),
|
||||||
)
|
)
|
||||||
149
cfg/sim_dil_cfg.py
Normal file
149
cfg/sim_dil_cfg.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
Node('sim_dil_test.test',
|
||||||
|
'simulated dil4 state machine test',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V1',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'condense valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V2',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'circuitshort valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V3',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'circuitshort valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V4',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'still valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V5',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'still valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V7',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V8',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V9',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V10',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V11A',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V12A',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V13A',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'dump valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('pump_He3',
|
||||||
|
'frappy_psi.sim_dil.Valve',
|
||||||
|
'rotary_pump_He3',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V14',
|
||||||
|
'frappy_psi.sim_dil.PulsedValve',
|
||||||
|
'pulsed valve',
|
||||||
|
value='close',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('V6_motor',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'motor valve',
|
||||||
|
value=Param(0.0, unit='%'),
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('G1',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'condensline pressure',
|
||||||
|
value=Param(0, unit='mbar')
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('G3',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'dump pressure',
|
||||||
|
value=Param(0, unit='mbar')
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('P1',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'still pressure',
|
||||||
|
value=Param(0, unit='mbar')
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('T_oneK',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'temp one Kelvin chamber',
|
||||||
|
value=Param(4, unit='K'),
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('T_still',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'temp still chamber',
|
||||||
|
value=Param(4, unit='K'),
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('T_mix',
|
||||||
|
'frappy_psi.sim_dil.Sensor',
|
||||||
|
'temp mix chamber',
|
||||||
|
value=Param(4, unit='K'),
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('dil',
|
||||||
|
'frappy_psi.dilution_new.DIL4',
|
||||||
|
'dilution state machine',
|
||||||
|
condenseline_pressure='G1', # G1
|
||||||
|
condense_valve='V1', # V1
|
||||||
|
dump_valve='V9', # V9
|
||||||
|
forepump='pump_He3', # rotary_pump_He3 (24)
|
||||||
|
condenseline_valve='V1', # V1
|
||||||
|
circuitshort_valve='V3', # V3
|
||||||
|
still_valve='V6_motor', # V6
|
||||||
|
pumpout_valve='V14', # V14
|
||||||
|
still_pressure='P1', # P1
|
||||||
|
dump_pressure='G3', # G3
|
||||||
|
oneK_temp='T_oneK',
|
||||||
|
still_temp='T_still',
|
||||||
|
mix_temp='T_mix',
|
||||||
|
sorb_pump_time=30,
|
||||||
|
)
|
||||||
37
cfg/stick/fibrestick_cfg.py
Normal file
37
cfg/stick/fibrestick_cfg.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
Node('fibrestick.psi.ch',
|
||||||
|
'stick with laser fibre',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('sea_stick',
|
||||||
|
'frappy_psi.sea.SeaClient',
|
||||||
|
'SEA stick connection',
|
||||||
|
config='fibre.stick',
|
||||||
|
service='stick',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('ts',
|
||||||
|
'frappy_psi.sea.SeaReadable', '',
|
||||||
|
meaning=['temperature', 30],
|
||||||
|
io='sea_stick',
|
||||||
|
sea_object='tt',
|
||||||
|
json_file='ma11.config.json',
|
||||||
|
rel_paths=['ts'],
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('laser_io',
|
||||||
|
'frappy_psi.pdld.IO',
|
||||||
|
'laser IO',
|
||||||
|
uri='serial:///dev/serial/by-path/pci-0000:00:14.0-usb-0:4.4.4.2:1.0-port0?baudrate=9600',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('laser',
|
||||||
|
'frappy_psi.pdld.Laser',
|
||||||
|
'laser switch',
|
||||||
|
io='laser_io',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('laser_power',
|
||||||
|
'frappy_psi.pdld.LaserPower',
|
||||||
|
'laser power',
|
||||||
|
io='laser_io',
|
||||||
|
)
|
||||||
17
cfg/test_ips_cfg.py
Normal file
17
cfg/test_ips_cfg.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Node('test_ips.psi.ch',
|
||||||
|
'ips test',
|
||||||
|
'tcp://5000',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('io',
|
||||||
|
'frappy_psi.oiclassic.IPS_IO',
|
||||||
|
'',
|
||||||
|
uri='ma11-ts:3002',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('B',
|
||||||
|
'frappy_psi.oiclassic.Field',
|
||||||
|
'magnetic field',
|
||||||
|
io='io',
|
||||||
|
target=Param(max=0.2),
|
||||||
|
)
|
||||||
@@ -3,10 +3,14 @@ Configuration File
|
|||||||
|
|
||||||
.. _node configuration:
|
.. _node configuration:
|
||||||
|
|
||||||
:Node(equipment_id, description, interface, \*\*kwds):
|
:Node:
|
||||||
|
|
||||||
Specify the SEC-node properties.
|
Specify the SEC-node properties.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
Node(equipment_id, description, interface, **kwds):
|
||||||
|
|
||||||
The arguments are SECoP node properties and additional internal node configurations
|
The arguments are SECoP node properties and additional internal node configurations
|
||||||
|
|
||||||
:Parameters:
|
:Parameters:
|
||||||
@@ -18,9 +22,14 @@ Configuration File
|
|||||||
|
|
||||||
.. _mod configuration:
|
.. _mod configuration:
|
||||||
|
|
||||||
:Mod(name, cls, description, \*\*kwds):
|
:Mod:
|
||||||
|
|
||||||
Create a SECoP module.
|
Create a SECoP module.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
Mod(name, cls, description, **kwds)
|
||||||
|
|
||||||
Keyworded argument matching a parameter name are used to configure
|
Keyworded argument matching a parameter name are used to configure
|
||||||
the initial value of a parameter. For configuring the parameter properties
|
the initial value of a parameter. For configuring the parameter properties
|
||||||
the value must be an instance of **Param**, using the keyworded arguments
|
the value must be an instance of **Param**, using the keyworded arguments
|
||||||
@@ -37,22 +46,60 @@ Configuration File
|
|||||||
|
|
||||||
.. _param configuration:
|
.. _param configuration:
|
||||||
|
|
||||||
:Param(value=<undef>, \*\*kwds):
|
:Param:
|
||||||
|
|
||||||
Configure a parameter
|
Configure a parameter
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
Param(value=<undef>, **kwds):
|
||||||
|
|
||||||
:Parameters:
|
:Parameters:
|
||||||
|
|
||||||
- **value** - if given, the initial value of the parameter
|
- **value** - if given, the initial value of the parameter
|
||||||
- **kwds** - parameter or datatype SECoP properties (see :class:`frappy.param.Parameter`
|
- **kwds** - parameter or datatype SECoP properties (see :class:`frappy.param.Parameter`
|
||||||
and :class:`frappy.datatypes.Datatypes`)
|
and :class:`frappy.datatypes.Datatypes`)
|
||||||
|
|
||||||
|
.. _io configuration:
|
||||||
|
|
||||||
|
:IO:
|
||||||
|
|
||||||
|
Configure IO modules (communicators)
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
IO(<io name>, <uri>, ...)
|
||||||
|
|
||||||
|
|
||||||
|
It is recommended that the class of the needed IO is specified as class
|
||||||
|
attribute ioClass on the modules class. In this case, for the configuration
|
||||||
|
of the IO modules only their name and URI is needed, for example:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
IO('io_T', 'tcp://192.168.1.1:7777', export=False)
|
||||||
|
IO('io_C', 'serial:///dev/tty_USB0&baudrate=9600', export=False)
|
||||||
|
|
||||||
|
Mod('T_sample', 'frappy_psi.lakeshore.TemperatureSensor', 'the sample T',
|
||||||
|
io='io_T', channel='C')
|
||||||
|
Mod('T_main', 'frappy_psi.lakeshore.TemperatureLoop', 'the main T',
|
||||||
|
io='io_T', channel='A')
|
||||||
|
Mod('C_sample', 'frappy_psi.ah2700.Capacitance', 'the sample capacitance',
|
||||||
|
io='io_C')
|
||||||
|
|
||||||
|
The ``export=False`` argument tells Frappy to hide both communicators.
|
||||||
|
|
||||||
|
|
||||||
.. _command configuration:
|
.. _command configuration:
|
||||||
|
|
||||||
:Command(\*\*kwds):
|
:Command:
|
||||||
|
|
||||||
Configure a command
|
Configure a command
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
Command(**kwds)
|
||||||
|
|
||||||
:Parameters:
|
:Parameters:
|
||||||
|
|
||||||
- **kwds** - command SECoP properties (see :class:`frappy.param.Commands`)
|
- **kwds** - command SECoP properties (see :class:`frappy.param.Commands`)
|
||||||
@@ -140,4 +140,4 @@ Exception classes
|
|||||||
.. automodule:: frappy.errors
|
.. automodule:: frappy.errors
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. include:: configuration.rst
|
.. include:: configuration.inc
|
||||||
@@ -99,16 +99,11 @@ We choose the name *example_cryo* and create therefore a configuration file
|
|||||||
Node('example_cryo.psi.ch', # a globally unique identification
|
Node('example_cryo.psi.ch', # a globally unique identification
|
||||||
'this is an example cryostat for the Frappy tutorial', # describes the node
|
'this is an example cryostat for the Frappy tutorial', # describes the node
|
||||||
interface='tcp://10767') # you might choose any port number > 1024
|
interface='tcp://10767') # you might choose any port number > 1024
|
||||||
Mod('io', # the name of the module
|
IO('io', 'serial://COM6:?baudrate=57600+parity=odd+bytesize=7')
|
||||||
'frappy_psi.lakeshore.LakeshoreIO', # the class used for communication
|
|
||||||
'communication to main controller', # a description
|
|
||||||
# the serial connection, including serial settings (see frappy.io.IOBase):
|
|
||||||
uri='serial://COM6:?baudrate=57600+parity=odd+bytesize=7',
|
|
||||||
)
|
|
||||||
Mod('T',
|
Mod('T',
|
||||||
'frappy_psi.lakeshore.TemperatureSensor',
|
'frappy_psi.lakeshore.TemperatureSensor',
|
||||||
'Sample Temperature',
|
'Sample Temperature',
|
||||||
io='io', # refers to above defined module 'io'
|
io='io', # refers to above defined io module called 'io'
|
||||||
channel='A', # the channel on the LakeShore for this module
|
channel='A', # the channel on the LakeShore for this module
|
||||||
value=Param(max=470), # alter the maximum expected T
|
value=Param(max=470), # alter the maximum expected T
|
||||||
)
|
)
|
||||||
@@ -120,8 +115,8 @@ Usually the only important value in the server address is the TCP port under whi
|
|||||||
server will be accessible. Currently only the tcp scheme is supported.
|
server will be accessible. Currently only the tcp scheme is supported.
|
||||||
|
|
||||||
Then for each module a :ref:`Mod <mod configuration>` section follows.
|
Then for each module a :ref:`Mod <mod configuration>` section follows.
|
||||||
We have to create the ``io`` module for communication first, with
|
But first we have to create the ``io`` module for communication.
|
||||||
the ``uri`` as its most important argument.
|
For this we use an :ref:`IO <io configuration>` section.
|
||||||
In case of a serial connection the prefix is ``serial://``. On a Windows machine, the full
|
In case of a serial connection the prefix is ``serial://``. On a Windows machine, the full
|
||||||
uri is something like ``serial://COM6:?baudrate=9600`` on a linux system it might be
|
uri is something like ``serial://COM6:?baudrate=9600`` on a linux system it might be
|
||||||
``serial:///dev/ttyUSB0?baudrate=9600``. In case of a LAN connection, the uri should
|
``serial:///dev/ttyUSB0?baudrate=9600``. In case of a LAN connection, the uri should
|
||||||
|
|||||||
@@ -114,14 +114,18 @@ class Collector:
|
|||||||
self.modules = {}
|
self.modules = {}
|
||||||
self.warnings = []
|
self.warnings = []
|
||||||
|
|
||||||
def add(self, *args, **kwds):
|
def add(self, name, cls, description, **kwds):
|
||||||
mod = Mod(*args, **kwds)
|
mod = Mod(name, cls, description, **kwds)
|
||||||
name = mod.pop('name')
|
name = mod.pop('name')
|
||||||
if name in self.modules:
|
if name in self.modules:
|
||||||
self.warnings.append(f'duplicate module {name} overrides previous')
|
self.warnings.append(f'duplicate module {name} overrides previous')
|
||||||
self.modules[name] = mod
|
self.modules[name] = mod
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
|
def add_io(self, name, uri, **kwds):
|
||||||
|
mod = Mod(name, cls='<auto>', description='', uri=uri, **kwds)
|
||||||
|
self.modules[mod.pop('name')] = mod
|
||||||
|
|
||||||
def override(self, name, **kwds):
|
def override(self, name, **kwds):
|
||||||
"""override properties/parameters of previously configured modules
|
"""override properties/parameters of previously configured modules
|
||||||
|
|
||||||
@@ -180,12 +184,37 @@ class Include:
|
|||||||
exec(compile(filename.read_bytes(), filename, 'exec'), self.namespace)
|
exec(compile(filename.read_bytes(), filename, 'exec'), self.namespace)
|
||||||
|
|
||||||
|
|
||||||
def process_file(filename, log):
|
def fix_io_modules(cfgdict, log):
|
||||||
config_text = filename.read_bytes()
|
node = cfgdict.pop('node')
|
||||||
|
io_modules = {k: [] for k, v in cfgdict.items() if v.get('cls') == '<auto>'}
|
||||||
|
for modname, modcfg in cfgdict.items():
|
||||||
|
ioname = modcfg.get('io', {}).get('value')
|
||||||
|
if ioname:
|
||||||
|
iocfg = cfgdict.get(ioname)
|
||||||
|
if iocfg:
|
||||||
|
referring_modules = io_modules.get(ioname)
|
||||||
|
if referring_modules is not None:
|
||||||
|
iocfg['cls'] = f"{modcfg['cls']}.ioClass"
|
||||||
|
referring_modules.append(modname)
|
||||||
|
|
||||||
|
# fix description:
|
||||||
|
for ioname, referring_modules in io_modules.items():
|
||||||
|
if referring_modules:
|
||||||
|
if not cfgdict[ioname]['description']:
|
||||||
|
cfgdict[ioname]['description'] = f"communicator for {', '.join(k for k in referring_modules)}"
|
||||||
|
else:
|
||||||
|
log.warning('remove unused io module %r', ioname)
|
||||||
|
cfgdict.pop(ioname)
|
||||||
|
cfgdict['node'] = node
|
||||||
|
|
||||||
|
|
||||||
|
def process_file(filename, log, config_text=None):
|
||||||
|
if config_text is None:
|
||||||
|
config_text = filename.read_bytes()
|
||||||
node = NodeCollector()
|
node = NodeCollector()
|
||||||
mods = Collector()
|
mods = Collector()
|
||||||
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group,
|
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group,
|
||||||
'override': mods.override, 'overrideNode': node.override}
|
'override': mods.override, 'overrideNode': node.override, 'IO': mods.add_io}
|
||||||
ns['include'] = Include(ns, log)
|
ns['include'] = Include(ns, log)
|
||||||
# pylint: disable=exec-used
|
# pylint: disable=exec-used
|
||||||
exec(compile(config_text, filename, 'exec'), ns)
|
exec(compile(config_text, filename, 'exec'), ns)
|
||||||
@@ -236,6 +265,7 @@ def load_config(cfgfiles, log):
|
|||||||
filename = to_config_path(str(cfgfile), log)
|
filename = to_config_path(str(cfgfile), log)
|
||||||
log.debug('Parsing config file %s...', filename)
|
log.debug('Parsing config file %s...', filename)
|
||||||
cfg = process_file(filename, log)
|
cfg = process_file(filename, log)
|
||||||
|
fix_io_modules(cfg, log)
|
||||||
if config:
|
if config:
|
||||||
config.merge_modules(cfg)
|
config.merge_modules(cfg)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class SimpleDataType(HasProperties):
|
|||||||
- StringType: the bare string is returned
|
- StringType: the bare string is returned
|
||||||
- EnumType: the name of the enum is returned
|
- EnumType: the name of the enum is returned
|
||||||
"""
|
"""
|
||||||
return self.format_value(value, False)
|
return value if isinstance(value, str) else repr(value)
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""if needed, reformat value for transport"""
|
"""if needed, reformat value for transport"""
|
||||||
@@ -1132,7 +1132,7 @@ class CommandType(DataType):
|
|||||||
|
|
||||||
# internally used datatypes (i.e. only for programming the SEC-node)
|
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||||
|
|
||||||
class DefaultType(DataType):
|
class DefaultType(SimpleDataType):
|
||||||
"""datatype used as default for parameters
|
"""datatype used as default for parameters
|
||||||
|
|
||||||
needs some minimal interface to avoid errors when
|
needs some minimal interface to avoid errors when
|
||||||
|
|||||||
@@ -234,22 +234,37 @@ def clamp(_min, value, _max):
|
|||||||
i.e. value if min <= value <= max, else min or max depending on which side
|
i.e. value if min <= value <= max, else min or max depending on which side
|
||||||
value lies outside the [min..max] interval. This works even when min > max!
|
value lies outside the [min..max] interval. This works even when min > max!
|
||||||
"""
|
"""
|
||||||
# return median, i.e. clamp the the value between min and max
|
# return median, i.e. clamp the value between min and max
|
||||||
return sorted([_min, value, _max])[1]
|
return sorted([_min, value, _max])[1]
|
||||||
|
|
||||||
|
|
||||||
def get_class(spec):
|
def get_class(spec):
|
||||||
"""loads a class given by string in dotted notation (as python would do)"""
|
"""loads an object given by string in dotted notation (as python would do)
|
||||||
modname, classname = spec.rsplit('.', 1)
|
|
||||||
if modname.startswith('frappy'):
|
import the specified module and get the specified item from it
|
||||||
module = importlib.import_module(modname)
|
examples: 'frappy_demo.lakeshore.TemperatureSensor', 'frappy.modules.Readable.Status'
|
||||||
else:
|
|
||||||
# rarely needed by now....
|
:param spec: a dot-separated list of module names followed by the name of
|
||||||
module = importlib.import_module('frappy.' + modname)
|
a class (or any object) and optionally names of attributes
|
||||||
try:
|
:return: the object
|
||||||
return getattr(module, classname)
|
"""
|
||||||
except AttributeError:
|
for maxsplit in range(1, len(spec)):
|
||||||
raise AttributeError('no such class') from None
|
# len(spec) is high enough for all cases
|
||||||
|
module, *attrs = spec.rsplit('.', maxsplit)
|
||||||
|
try:
|
||||||
|
obj = importlib.import_module(module)
|
||||||
|
break
|
||||||
|
except ImportError:
|
||||||
|
if '.' in module:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
for na, attr in enumerate(attrs):
|
||||||
|
try:
|
||||||
|
obj = getattr(obj, attr)
|
||||||
|
except AttributeError:
|
||||||
|
print(na, attrs)
|
||||||
|
raise AttributeError(f'{".".join(attrs[:na+1])!r} not found in {module!r}') from None
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def mkthread(func, *args, **kwds):
|
def mkthread(func, *args, **kwds):
|
||||||
@@ -477,3 +492,15 @@ def delayed_import(modname):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return _Raiser(modname)
|
return _Raiser(modname)
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class LazyImport:
|
||||||
|
module = None
|
||||||
|
|
||||||
|
def __init__(self, modulename):
|
||||||
|
self.modulename = modulename
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if self.module is None:
|
||||||
|
self.module = __import__(self.modulename)
|
||||||
|
return getattr(self.module, name)
|
||||||
@@ -37,6 +37,13 @@ class MathParser:
|
|||||||
ast.Div: op.truediv,
|
ast.Div: op.truediv,
|
||||||
ast.Pow: op.pow,
|
ast.Pow: op.pow,
|
||||||
ast.FloorDiv: op.floordiv,
|
ast.FloorDiv: op.floordiv,
|
||||||
|
ast.Lt: op.lt,
|
||||||
|
ast.Gt: op.gt,
|
||||||
|
ast.LtE: op.le,
|
||||||
|
ast.GtE: op.ge,
|
||||||
|
ast.Eq: op.eq,
|
||||||
|
ast.NotEq: op.ne,
|
||||||
|
ast.Not: op.not_,
|
||||||
ast.USub: op.neg,
|
ast.USub: op.neg,
|
||||||
ast.UAdd: lambda a:a}
|
ast.UAdd: lambda a:a}
|
||||||
|
|
||||||
@@ -74,6 +81,15 @@ class MathParser:
|
|||||||
if isinstance(node, ast.BinOp): # evaluate binary operations
|
if isinstance(node, ast.BinOp): # evaluate binary operations
|
||||||
method = self._operators2method[type(node.op)]
|
method = self._operators2method[type(node.op)]
|
||||||
return method( self.eval_(node.left), self.eval_(node.right))
|
return method( self.eval_(node.left), self.eval_(node.right))
|
||||||
|
if isinstance(node, ast.Compare): # evaluate binary operations
|
||||||
|
left = self.eval_(node.left)
|
||||||
|
for oper, value in zip(node.ops, node.comparators):
|
||||||
|
method = self._operators2method[type(oper)]
|
||||||
|
right = self.eval_(value)
|
||||||
|
if not method(left, right):
|
||||||
|
return False
|
||||||
|
left = right
|
||||||
|
return True
|
||||||
if isinstance(node, ast.UnaryOp): # handle operators
|
if isinstance(node, ast.UnaryOp): # handle operators
|
||||||
method = self._operators2method[type(node.op)]
|
method = self._operators2method[type(node.op)]
|
||||||
return method( self.eval_(node.operand) )
|
return method( self.eval_(node.operand) )
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ class SecNode:
|
|||||||
- get_module(modulename) returns the requested module or None if there is
|
- get_module(modulename) returns the requested module or None if there is
|
||||||
no suitable configuration on the server
|
no suitable configuration on the server
|
||||||
"""
|
"""
|
||||||
raise_config_errors = False # collect catchable errors instead of raising
|
|
||||||
|
|
||||||
def __init__(self, name, logger, options, srv):
|
def __init__(self, name, logger, options, srv):
|
||||||
self.equipment_id = options.pop('equipment_id', name)
|
self.equipment_id = options.pop('equipment_id', name)
|
||||||
self.nodeprops = {}
|
self.nodeprops = {}
|
||||||
@@ -177,7 +175,7 @@ class SecNode:
|
|||||||
try:
|
try:
|
||||||
getattr(modobj, prop)
|
getattr(modobj, prop)
|
||||||
except SECoPError as e:
|
except SECoPError as e:
|
||||||
if self.raise_config_errors:
|
if generalConfig.raise_config_errors:
|
||||||
raise
|
raise
|
||||||
self.error_count += 1
|
self.error_count += 1
|
||||||
modobj.logError(e)
|
modobj.logError(e)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ except ImportError:
|
|||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
INTERFACES = {
|
INTERFACES = {
|
||||||
'tcp': 'protocol.interface.tcp.TCPServer',
|
'tcp': 'frappy.protocol.interface.tcp.TCPServer',
|
||||||
'ws': 'protocol.interface.ws.WSServer',
|
'ws': 'frappy.protocol.interface.ws.WSServer',
|
||||||
}
|
}
|
||||||
_restart = True
|
_restart = True
|
||||||
|
|
||||||
|
|||||||
0
frappy/tools/__init__.py
Normal file
0
frappy/tools/__init__.py
Normal file
948
frappy/tools/cfgedit.py
Normal file
948
frappy/tools/cfgedit.py
Normal file
@@ -0,0 +1,948 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
from pathlib import Path
|
||||||
|
from psutil import pid_exists
|
||||||
|
from frappy.errors import ConfigError
|
||||||
|
from frappy.lib import generalConfig
|
||||||
|
from frappy.lib import mkthread, formatExtendedTraceback
|
||||||
|
from frappy.config import process_file, to_config_path
|
||||||
|
from frappy.tools.configdata import Value, cfgdata_to_py, cfgdata_from_py, get_datatype, site, ModuleClass, stringtype
|
||||||
|
from frappy.tools.completion import class_completion, recommended_prs
|
||||||
|
import frappy.tools.terminalgui as tg
|
||||||
|
from frappy.tools.terminalgui import Main, MenuItem, TextEdit, PushButton, ModalDialog, KEY
|
||||||
|
|
||||||
|
|
||||||
|
KEY.add(
|
||||||
|
TOGGLE_DETAILED='^t',
|
||||||
|
NEXT_VERSION='^n',
|
||||||
|
PREV_VERSION='^b',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
VERSION_SEPARATOR = "\n''''"
|
||||||
|
TIMESTAMP_FMT = '%Y-%m-%d-%H%M%S'
|
||||||
|
|
||||||
|
|
||||||
|
def get_timestamp(file):
|
||||||
|
return time.strftime(TIMESTAMP_FMT, time.localtime(file.stat().st_mtime))
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# - use also shift-Tab for level up?
|
||||||
|
|
||||||
|
|
||||||
|
def unix_cmd(cmd, *args):
|
||||||
|
out = Popen(cmd.split() + list(args), stdout=PIPE).communicate()[0]
|
||||||
|
return list(out.decode().split('\n'))
|
||||||
|
|
||||||
|
|
||||||
|
class StringValue: # TODO: unused?
|
||||||
|
error = None
|
||||||
|
|
||||||
|
def __init__(self, value, from_string=False, datatype=None):
|
||||||
|
self.strvalue = value
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
self.strvalue = value
|
||||||
|
|
||||||
|
def set_from_string(self, strvalue):
|
||||||
|
self.strvalue = strvalue
|
||||||
|
|
||||||
|
def get_repr(self):
|
||||||
|
return repr(self.strvalue)
|
||||||
|
|
||||||
|
|
||||||
|
class TopWidget:
|
||||||
|
parent_cls = Main
|
||||||
|
|
||||||
|
|
||||||
|
class Child(tg.Widget):
|
||||||
|
"""child widget of NodeWidget ot ModuleWidget"""
|
||||||
|
parent = TopWidget
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def collect(self, cfgdict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_data(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class HasValue(Child):
|
||||||
|
clsobj = None
|
||||||
|
|
||||||
|
def init_value_widget(self, parent, valobj):
|
||||||
|
self.init_parent(parent)
|
||||||
|
self.valobj = valobj
|
||||||
|
|
||||||
|
def validate(self, strvalue, main=None):
|
||||||
|
pname = self.get_name()
|
||||||
|
valobj = self.valobj
|
||||||
|
prev = valobj.value, valobj.strvalue, valobj.error
|
||||||
|
try:
|
||||||
|
if pname != 'cls':
|
||||||
|
if self.clsobj != self.parent.clsobj:
|
||||||
|
self.clsobj = self.parent.clsobj
|
||||||
|
valobj.datatype, valobj.error = get_datatype(
|
||||||
|
self.get_name(), self.clsobj, valobj.value)
|
||||||
|
valobj.validate_from_string(strvalue)
|
||||||
|
self.error = None
|
||||||
|
except Exception as e:
|
||||||
|
self.error = str(e)
|
||||||
|
if main and (valobj.value, valobj.strvalue, valobj.error) != prev:
|
||||||
|
main.touch()
|
||||||
|
return valobj.strvalue
|
||||||
|
|
||||||
|
def check_data(self):
|
||||||
|
self.validate(self.valobj.strvalue)
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return self.get_name() and self.valobj.strvalue
|
||||||
|
|
||||||
|
|
||||||
|
class ValueWidget(HasValue, tg.LineEdit):
|
||||||
|
fixedname = None
|
||||||
|
|
||||||
|
def __init__(self, parent, name, valobj, label=None):
|
||||||
|
"""init a value widget
|
||||||
|
|
||||||
|
:param parent: the parent widget
|
||||||
|
:param name: the initial name
|
||||||
|
:param valobj: the object containing value and datatype
|
||||||
|
:param label: None: the name is changeable, else: a label (which might be different from name)
|
||||||
|
"""
|
||||||
|
self.init_value_widget(parent, valobj)
|
||||||
|
if label is not None:
|
||||||
|
labelwidget = tg.TextWidget(label)
|
||||||
|
self.fixedname = name
|
||||||
|
else:
|
||||||
|
labelwidget = tg.NameEdit(name, self.validate_name)
|
||||||
|
# self.log.info('value widget %r %r', name, self.fixedname)
|
||||||
|
if valobj.completion:
|
||||||
|
valueedit = tg.TextEditCompl(valobj.strvalue, self.validate, valobj.completion)
|
||||||
|
else:
|
||||||
|
valueedit = TextEdit(valobj.strvalue, self.validate)
|
||||||
|
super().__init__(labelwidget, valueedit)
|
||||||
|
|
||||||
|
def validate_name(self, name, main):
|
||||||
|
widget_dict = self.parent.widget_dict
|
||||||
|
if name.isidentifier():
|
||||||
|
other = widget_dict.get(name)
|
||||||
|
if other and other != self:
|
||||||
|
self.error = f'duplicate name {name!r}'
|
||||||
|
return self.get_name()
|
||||||
|
self.clsobj = None
|
||||||
|
self.error = None
|
||||||
|
widget = widget_dict.pop(self.get_name(), None)
|
||||||
|
if widget:
|
||||||
|
widget_dict[name] = widget
|
||||||
|
else:
|
||||||
|
self.error = f'illegal name {name!r}'
|
||||||
|
return self.get_name()
|
||||||
|
return name
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
if self.fixedname:
|
||||||
|
return self.fixedname
|
||||||
|
return self.labelwidget.value
|
||||||
|
|
||||||
|
def collect(self, as_dict):
|
||||||
|
"""collect data"""
|
||||||
|
name = self.get_name()
|
||||||
|
if name:
|
||||||
|
as_dict[name] = self.valobj
|
||||||
|
|
||||||
|
|
||||||
|
class DocWidget(HasValue, tg.MultiLineEdit):
|
||||||
|
parent_cls = TopWidget
|
||||||
|
|
||||||
|
def __init__(self, parent, name, valobj):
|
||||||
|
self.init_value_widget(parent, valobj)
|
||||||
|
self.valobj = valobj
|
||||||
|
self.name = name
|
||||||
|
super().__init__(name, valobj.strvalue, 'doc: ')
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def collect(self, config):
|
||||||
|
self.valobj.set_value(self.value)
|
||||||
|
config[self.name] = self.valobj
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWidget(TopWidget, tg.Container):
|
||||||
|
"""base for Module or Node"""
|
||||||
|
clsobj = None
|
||||||
|
header = None
|
||||||
|
special_names = 'name', 'cls', 'description'
|
||||||
|
endline_help = 'RET: add module p: add property'
|
||||||
|
|
||||||
|
def init(self, parent):
|
||||||
|
self.widgets = []
|
||||||
|
self.init_parent(parent, EditorMain)
|
||||||
|
self.focus = 0
|
||||||
|
self.widget_dict = {}
|
||||||
|
self.fixed_names = self.get_fixed_names()
|
||||||
|
|
||||||
|
def get_menu(self):
|
||||||
|
main = self.parent
|
||||||
|
if main.version_view:
|
||||||
|
return main.get_menu()
|
||||||
|
return self.context_menu + main.get_menu()
|
||||||
|
|
||||||
|
def get_fixed_names(self):
|
||||||
|
result = {k: k for k in self.special_names}
|
||||||
|
result['name'] = self.header
|
||||||
|
return result
|
||||||
|
|
||||||
|
def add_widget(self, name, valobj, pos=None):
|
||||||
|
label = self.fixed_names.get(name)
|
||||||
|
widget = ValueWidget(self, name, valobj, label)
|
||||||
|
self.widget_dict[name] = widget
|
||||||
|
# self.log.info('add widget %r: label=%r name=%r', name, label, widget.get_name())
|
||||||
|
if pos is None:
|
||||||
|
self.widgets.append(widget)
|
||||||
|
else:
|
||||||
|
if pos < 0:
|
||||||
|
pos += len(self.widgets)
|
||||||
|
self.widgets.insert(pos, widget)
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def new_widget(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def insert_module(self, module, after_current=False):
|
||||||
|
main = self.parent
|
||||||
|
main.insert(main.focus + after_current, module)
|
||||||
|
main.set_focus(main.focus + 1)
|
||||||
|
if not after_current:
|
||||||
|
self.set_focus(1)
|
||||||
|
main.advance(-1)
|
||||||
|
# module.set_focus(0) # go to name widget
|
||||||
|
|
||||||
|
def add_module(self, after_current=False):
|
||||||
|
modcfg = {'name': Value(''), 'cls': Value(f'{site.frappy_subdir}.'), 'description': Value('')}
|
||||||
|
self.insert_module(ModuleWidget(self.parent, '', modcfg), after_current)
|
||||||
|
|
||||||
|
def add_iomodule(self, after_current=False):
|
||||||
|
modcfg = {'name': Value(''), 'uri': Value('')}
|
||||||
|
self.insert_module(IOWidget(self.parent, '', modcfg), after_current)
|
||||||
|
|
||||||
|
def get_widget_value(self, key):
|
||||||
|
try:
|
||||||
|
return self.widget_dict[key].valobj.strvalue
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.get_widget_value('name')
|
||||||
|
|
||||||
|
def draw_summary(self, wr, in_focus):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def draw(self, wr, in_focus=False):
|
||||||
|
main = self.parent
|
||||||
|
wr.set_leftwidth(main.leftwidth)
|
||||||
|
|
||||||
|
if main.detailed:
|
||||||
|
self.draw_widgets(wr, in_focus)
|
||||||
|
else:
|
||||||
|
self.draw_summary(wr, in_focus)
|
||||||
|
|
||||||
|
def collect(self, result):
|
||||||
|
name = self.get_name()
|
||||||
|
if name:
|
||||||
|
result[name] = modcfg = {}
|
||||||
|
for w in self.widgets:
|
||||||
|
w.collect(modcfg)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleName(Value):
|
||||||
|
def __init__(self, main, name):
|
||||||
|
self.main = main
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
def validate_from_string(self, value):
|
||||||
|
if not value:
|
||||||
|
self.strvalue = self.value = ''
|
||||||
|
raise ValueError('empty name')
|
||||||
|
if value != self.value and value in self.main.widget_dict:
|
||||||
|
self.strvalue = self.value = ''
|
||||||
|
raise ValueError(f'duplicate name {value!r}')
|
||||||
|
self.value = self.strvalue = value
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleWidget(BaseWidget):
|
||||||
|
header = 'Module'
|
||||||
|
endline_help = 'RET: add module i: add io module p: add parameter or property'
|
||||||
|
|
||||||
|
def __init__(self, parent, name, modulecfg):
|
||||||
|
assert name == modulecfg['name'].value
|
||||||
|
modulecfg['name'] = ModuleName(parent, name)
|
||||||
|
self.init(parent)
|
||||||
|
self.context_menu = [
|
||||||
|
MenuItem('add parameter/property', 'p', self.new_widget),
|
||||||
|
MenuItem('add module', 'm', self.add_module),
|
||||||
|
MenuItem('add io module', 'i', self.add_iomodule),
|
||||||
|
MenuItem('purge empty prs', 'e', self.purge_prs),
|
||||||
|
MenuItem('add recommended prs', '+', self.complete_prs),
|
||||||
|
MenuItem('cut module', KEY.CUT, parent.cut_module),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.configure_class(modulecfg.get('cls'))
|
||||||
|
|
||||||
|
for name, valobj in modulecfg.items():
|
||||||
|
self.add_widget(name, valobj)
|
||||||
|
self.widgets.append(EndLine(self))
|
||||||
|
|
||||||
|
def configure_class(self, clsvalue):
|
||||||
|
clsvalue.callback = self.update_cls
|
||||||
|
clsvalue.completion = class_completion
|
||||||
|
clsobj = clsvalue.value
|
||||||
|
if clsobj:
|
||||||
|
self.fixed_names.update({k: k for k, v in recommended_prs(clsobj).items() if v})
|
||||||
|
|
||||||
|
def new_widget(self, name='', pos=None):
|
||||||
|
self.add_widget(name, Value('', *get_datatype('', self.clsobj, ''), from_string=True), self.focus)
|
||||||
|
|
||||||
|
def update_widget_dict(self):
|
||||||
|
self.widget_dict = {w.get_name(): w for w in self.widgets}
|
||||||
|
|
||||||
|
def get_name_info(self):
|
||||||
|
return self.clsobj, self.widget_dict
|
||||||
|
|
||||||
|
def handle(self, main):
|
||||||
|
while True:
|
||||||
|
key = super().handle(main) if main.detailed else main.get_key()
|
||||||
|
if key in (KEY.RIGHT, KEY.TAB):
|
||||||
|
main.detailed = True
|
||||||
|
main.status('')
|
||||||
|
main.offset = None # recalculate offset from screen pos
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
|
||||||
|
def current_row(self):
|
||||||
|
main = self.parent
|
||||||
|
return super().current_row() if main.detailed else 0
|
||||||
|
|
||||||
|
def height(self, to_focus=None):
|
||||||
|
main = self.parent
|
||||||
|
return super().height(to_focus) if main.detailed else 1
|
||||||
|
|
||||||
|
def check_data(self):
|
||||||
|
"""check clsobj is valid and check all params and props"""
|
||||||
|
# clswidget, = self.find_widgets('cls')
|
||||||
|
# clsobj = clswidget.valobj.value
|
||||||
|
for widget in self.widgets:
|
||||||
|
widget.check_data()
|
||||||
|
|
||||||
|
def update_cls(self, cls):
|
||||||
|
if cls != self.clsobj:
|
||||||
|
self.complete_prs(True)
|
||||||
|
self.clsobj = cls
|
||||||
|
self.check_data()
|
||||||
|
return cls
|
||||||
|
|
||||||
|
def complete_prs(self, only_mandatory=False):
|
||||||
|
if self.clsobj:
|
||||||
|
fixed_names = self.get_fixed_names()
|
||||||
|
names = set(w.get_name() for w in self.widgets)
|
||||||
|
for name, mandatory in recommended_prs(self.clsobj).items():
|
||||||
|
if mandatory:
|
||||||
|
fixed_names[name] = name
|
||||||
|
if name not in names and mandatory >= only_mandatory:
|
||||||
|
valobj = Value('', *get_datatype(name, self.clsobj, ''))
|
||||||
|
if name == 'cls':
|
||||||
|
self.log.info('add needed %r', valobj)
|
||||||
|
widget = self.add_widget(name, valobj, -1)
|
||||||
|
if mandatory:
|
||||||
|
widget.error = 'please set this mandatory property'
|
||||||
|
self.fixed_names = fixed_names
|
||||||
|
self.update_widget_dict()
|
||||||
|
|
||||||
|
def purge_prs(self):
|
||||||
|
self.widgets = [w for w in self.widgets if w.get_name() not in self.fixed_names and w.is_valid()]
|
||||||
|
self.update_widget_dict()
|
||||||
|
|
||||||
|
def draw_summary_right(self, wr):
|
||||||
|
half = (wr.width - wr.col) // 2
|
||||||
|
wr.norm(f"{self.get_widget_value('description').ljust(half)} {self.get_widget_value('cls')} ")
|
||||||
|
|
||||||
|
def draw_summary(self, wr, in_focus):
|
||||||
|
wr.startrow()
|
||||||
|
wr.norm(self.header.ljust(7))
|
||||||
|
name = self.get_widget_value('name')
|
||||||
|
if in_focus:
|
||||||
|
wr.set_cursor_pos()
|
||||||
|
wr.bright(name, round(wr.width * 0.2))
|
||||||
|
else:
|
||||||
|
wr.norm(name.ljust(round(wr.width * 0.2)))
|
||||||
|
self.draw_summary_right(wr)
|
||||||
|
|
||||||
|
def collect(self, result):
|
||||||
|
super().collect(result)
|
||||||
|
name = self.get_name()
|
||||||
|
if name:
|
||||||
|
assert result[name].pop('name').value == name
|
||||||
|
|
||||||
|
|
||||||
|
class IOWidget(ModuleWidget):
|
||||||
|
header = 'IO'
|
||||||
|
endline_help = 'RET: add module p: add property'
|
||||||
|
special_names = 'name', 'uri'
|
||||||
|
|
||||||
|
def __init__(self, parent, name, modulecfg):
|
||||||
|
urivalue = modulecfg.get('uri')
|
||||||
|
if urivalue is None:
|
||||||
|
modulecfg['uri'] = Value('uri', stringtype)
|
||||||
|
super().__init__(parent, name, modulecfg)
|
||||||
|
|
||||||
|
def add_widget(self, name, valobj, pos=None):
|
||||||
|
if name != 'cls' and (
|
||||||
|
name != 'description' or valobj.strvalue):
|
||||||
|
super().add_widget(name, valobj)
|
||||||
|
|
||||||
|
def draw_summary_right(self, wr):
|
||||||
|
half = (wr.width - wr.col) // 2
|
||||||
|
ioname = self.get_name()
|
||||||
|
modules = [w.get_name() for w in self.parent.widgets if w.get_widget_value('io') == ioname]
|
||||||
|
wr.norm(f"{self.get_widget_value('uri').ljust(half)} for {','.join(modules)} ")
|
||||||
|
|
||||||
|
def collect(self, result):
|
||||||
|
name = self.get_name()
|
||||||
|
if name:
|
||||||
|
super().collect(result)
|
||||||
|
modcfg = result[name]
|
||||||
|
modcfg['cls'] = Value('<auto>', stringtype)
|
||||||
|
modcfg.setdefault('description', Value('', stringtype))
|
||||||
|
|
||||||
|
def configure_class(self, clsvalue):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndLine(Child):
|
||||||
|
parent_cls = TopWidget
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
self.init_parent(parent)
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def draw(self, wr, in_focus=False):
|
||||||
|
wr.startrow()
|
||||||
|
if in_focus:
|
||||||
|
wr.set_cursor_pos(wr.leftwidth)
|
||||||
|
wr.col = wr.leftwidth
|
||||||
|
wr.high(self.parent.endline_help)
|
||||||
|
|
||||||
|
def collect(self, result):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_data(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle(self, main):
|
||||||
|
self.showhelp = False
|
||||||
|
while True:
|
||||||
|
key = main.get_key()
|
||||||
|
if key in (KEY.RETURN, KEY.ENTER):
|
||||||
|
self.parent.add_module(True)
|
||||||
|
elif key in (KEY.UP, KEY.DOWN, KEY.QUIT):
|
||||||
|
return key
|
||||||
|
elif key == 'i':
|
||||||
|
self.parent.add_iomodule(True)
|
||||||
|
elif key == 'p':
|
||||||
|
self.parent.new_widget()
|
||||||
|
return KEY.GOTO_MAIN
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class NodeName(Value):
|
||||||
|
def __init__(self, main, name):
|
||||||
|
self.main = main
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
def validate_from_string(self, value):
|
||||||
|
try:
|
||||||
|
self.main.set_node_name(value)
|
||||||
|
except Exception:
|
||||||
|
self.value = self.strvalue = self.main.cfgname
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class NodeWidget(BaseWidget):
|
||||||
|
header = 'Node'
|
||||||
|
special_names = 'name', 'equipment_id', 'interface', 'title', 'doc'
|
||||||
|
summ_edit = {'name', 'title', 'doc'} # editable widgets in summary
|
||||||
|
|
||||||
|
def __init__(self, parent, name, nodecfg):
|
||||||
|
nodecfg['name'] = NodeName(parent, name)
|
||||||
|
self.init(parent)
|
||||||
|
self.context_menu = [
|
||||||
|
MenuItem('add parameter/property', 'p', self.new_widget),
|
||||||
|
# MenuItem('select line', '^K', self.select, None),
|
||||||
|
]
|
||||||
|
for name, valobj in nodecfg.items():
|
||||||
|
if name == 'doc':
|
||||||
|
docwidget = DocWidget(self, name, valobj)
|
||||||
|
self.widgets.append(docwidget)
|
||||||
|
self.widget_dict['doc'] = docwidget
|
||||||
|
else:
|
||||||
|
self.add_widget(name, valobj)
|
||||||
|
self.widgets.append(EndLine(self))
|
||||||
|
|
||||||
|
def new_widget(self, name=''):
|
||||||
|
"""insert new widget at focus pos"""
|
||||||
|
self.add_widget(name, Value('', None, from_string=True), self.focus)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return 'node'
|
||||||
|
|
||||||
|
def set_focus(self, focus, step=1):
|
||||||
|
while super().set_focus(focus, step):
|
||||||
|
if self.parent.detailed or self.get_focus_widget().get_name() in self.summ_edit:
|
||||||
|
return True
|
||||||
|
focus = self.focus + step
|
||||||
|
|
||||||
|
def height(self, to_focus=None):
|
||||||
|
main = self.parent
|
||||||
|
if not main.detailed:
|
||||||
|
return super().height(to_focus)
|
||||||
|
height = 0
|
||||||
|
if to_focus is None:
|
||||||
|
to_focus = len(self.widgets)
|
||||||
|
for nr, widget in enumerate(self.widgets[:to_focus]):
|
||||||
|
name = widget.get_name()
|
||||||
|
if name in self.summ_edit:
|
||||||
|
height += widget.height()
|
||||||
|
return height
|
||||||
|
|
||||||
|
def draw_summary(self, wr, in_focus):
|
||||||
|
# wr.startrow()
|
||||||
|
# wr.norm('Node ')
|
||||||
|
wr.set_leftwidth(7)
|
||||||
|
focus = self.focus if in_focus else None
|
||||||
|
for nr, widget in enumerate(self.widgets):
|
||||||
|
name = widget.get_name()
|
||||||
|
if name in self.summ_edit:
|
||||||
|
widget.draw(wr, nr == focus)
|
||||||
|
|
||||||
|
|
||||||
|
class SaveDialog(ModalDialog):
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.fileedit = tg.DialogInput(self, 'file', str(filename))
|
||||||
|
self.result = None
|
||||||
|
super().__init__([self.fileedit,
|
||||||
|
PushButton(self, 'save and quit', self.save),
|
||||||
|
PushButton(self, 'quit without saving', self.nosave),
|
||||||
|
PushButton(self, 'cancel', self.cancel)])
|
||||||
|
|
||||||
|
def execute(self, main):
|
||||||
|
self.set_focus(1) # go to save button
|
||||||
|
super().execute(main)
|
||||||
|
if self.fileedit.result:
|
||||||
|
return self.save()
|
||||||
|
if self.result is None:
|
||||||
|
# no button was pressed
|
||||||
|
return self.cancel()
|
||||||
|
return self.result()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.filename = self.fileedit.get_value()
|
||||||
|
return KEY.QUIT
|
||||||
|
|
||||||
|
def nosave(self):
|
||||||
|
self.filename = None
|
||||||
|
return KEY.QUIT
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
HELP_TEXT = """
|
||||||
|
Frappy Configuration Editor
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
A configuration files has a Node section, followed by any number of IO and
|
||||||
|
Module sections. IO section typically just contain the name and an uri.
|
||||||
|
A Module sections key item is the 'cls', denoting the python class for
|
||||||
|
the implementation. Entering the class is supported by a completion popup
|
||||||
|
menu, which opens as soon as you start typing.
|
||||||
|
When opening a file, the editor is in summary mode, showing a compact
|
||||||
|
overview over all modules. Use ctrl-T to toggle to detailed view to
|
||||||
|
be able to edit individual items.
|
||||||
|
|
||||||
|
|
||||||
|
Modify entries
|
||||||
|
--------------
|
||||||
|
|
||||||
|
To enter a new value a field, start typing. To modify a value press ctrl-A
|
||||||
|
of ctrl-E to go the the start or end of the string.
|
||||||
|
|
||||||
|
|
||||||
|
Context Menu
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Press ctrl-X to open a context menu. Navigate to an entry an press RETURN
|
||||||
|
or press the key indicated to the left to execute an action. A key starting
|
||||||
|
with ^ indicates to the given action may be performed with a ctrl-<key>
|
||||||
|
directly without preceding ctrl-X. However, within a context menu,
|
||||||
|
pressing the letter without ctrl works also.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class EditorMain(Main):
|
||||||
|
name = 'Main'
|
||||||
|
detailed = False
|
||||||
|
tmppath = None
|
||||||
|
help_text = HELP_TEXT
|
||||||
|
version_view = 0 # current version or when > 0 previous versions (not editable)
|
||||||
|
completion_widget = None # widget currently running a thread for guesses
|
||||||
|
leftwidth = 0.15
|
||||||
|
cut_modules = ()
|
||||||
|
cut_extend = False
|
||||||
|
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.titlebar = tg.TitleBar('Frappy Cfg Editor')
|
||||||
|
super().__init__([], tg.Writer, [self.titlebar], [tg.StatusBar(self)])
|
||||||
|
# self.select_menu = MenuItem('select module', CUT_KEY)
|
||||||
|
self.version_menu = [
|
||||||
|
MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
|
||||||
|
MenuItem('next version', KEY.NEXT_VERSION, self.next_version),
|
||||||
|
MenuItem('restore this version', 'r', self.restore_version),
|
||||||
|
MenuItem('copy module', KEY.CUT, self.cut_module),
|
||||||
|
]
|
||||||
|
self.main_menu = [
|
||||||
|
MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
|
||||||
|
]
|
||||||
|
self.detailed_menuitem = MenuItem('toggle detailed', KEY.TOGGLE_DETAILED, self.toggle_detailed)
|
||||||
|
self.cut_menuitem = MenuItem('insert cut modules', KEY.PASTE, self.insert_cut)
|
||||||
|
self.version_dir = Path('~/.local/share/frappy_config_editor').expanduser()
|
||||||
|
self.version_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
self.cfgname = None
|
||||||
|
self.pidfile = None
|
||||||
|
self.dirty = False
|
||||||
|
# cleanup pidfiles
|
||||||
|
for file in self.version_dir.glob('*.pid'):
|
||||||
|
pidstr = file.read_text()
|
||||||
|
if not pid_exists(int(pidstr)):
|
||||||
|
file.unlink()
|
||||||
|
cfgpath = Path(cfg)
|
||||||
|
if cfg != cfgpath.stem: # this is a filename
|
||||||
|
if cfg.endswith('_cfg.py'):
|
||||||
|
cfg = cfgpath.name[:-7]
|
||||||
|
else:
|
||||||
|
cfg = self.cfgpath.stem
|
||||||
|
else:
|
||||||
|
cfgpath = None
|
||||||
|
self.filecontent = None
|
||||||
|
self.set_node_name(cfg, cfgpath)
|
||||||
|
self.init_from_content(self.filecontent)
|
||||||
|
self.module_clipboard = {}
|
||||||
|
self.pr_clipboard = {}
|
||||||
|
|
||||||
|
def get_menu(self):
|
||||||
|
self.detailed_menuitem.name = 'summary view' if self.detailed else 'detailed view'
|
||||||
|
menu = self.version_menu if self.version_view else self.main_menu
|
||||||
|
if self.cut_modules and not self.version_view:
|
||||||
|
self.cut_menuitem.name = f'insert {self.describe_buffer()}'
|
||||||
|
menu.append(self.cut_menuitem)
|
||||||
|
return menu + [self.detailed_menuitem] + self.context_menu
|
||||||
|
|
||||||
|
def init_from_content(self, filecontent):
|
||||||
|
widgets = []
|
||||||
|
nodedata, moddata = cfgdata_from_py(self.cfgname, self.cfgpath, filecontent, self.log)
|
||||||
|
widgets.append(NodeWidget(self, self.cfgname, nodedata))
|
||||||
|
for key, modcfg in moddata.items():
|
||||||
|
clsvalue = modcfg.get('cls')
|
||||||
|
if clsvalue:
|
||||||
|
if clsvalue.strvalue == '<auto>':
|
||||||
|
widgets.append(IOWidget(self, key, modcfg))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
modcfg['cls'] = Value('', ModuleClass, from_string=True)
|
||||||
|
widgets.append(ModuleWidget(self, key, modcfg))
|
||||||
|
self.widgets = widgets
|
||||||
|
# self.dirty = False
|
||||||
|
|
||||||
|
def toggle_detailed(self):
|
||||||
|
self.detailed = not self.detailed
|
||||||
|
self.offset = None # recalculate offset from screen pos
|
||||||
|
self.status(None)
|
||||||
|
|
||||||
|
def get_key(self):
|
||||||
|
if self.dirty:
|
||||||
|
if not self.version_view:
|
||||||
|
self.save()
|
||||||
|
self.dirty = False
|
||||||
|
while True:
|
||||||
|
if self.completion_widget:
|
||||||
|
key = super().get_key(0.1)
|
||||||
|
if key is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
key = super().get_key()
|
||||||
|
if self.version_view:
|
||||||
|
if isinstance(key, str) or key in [KEY.DEL]:
|
||||||
|
self.status('', 'can not edit previous version')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return key
|
||||||
|
|
||||||
|
def describe_buffer(self,):
|
||||||
|
cm = self.cut_modules
|
||||||
|
if not cm:
|
||||||
|
return ''
|
||||||
|
if len(cm) > 1:
|
||||||
|
sep = ',' if len(cm) == 2 else '..'
|
||||||
|
return f'modules {cm[0].get_name()}{sep}{cm[-1].get_name()}'
|
||||||
|
return f'module {cm[0].get_name()}'
|
||||||
|
|
||||||
|
def cut_module(self):
|
||||||
|
if not self.cut_modules:
|
||||||
|
self.cut_extend = False
|
||||||
|
module = self.get_focus_widget()
|
||||||
|
if not isinstance(module, ModuleWidget):
|
||||||
|
self.status('', warn='can not cut node')
|
||||||
|
return
|
||||||
|
if not self.version_view:
|
||||||
|
self.widgets[self.focus:self.focus+1] = []
|
||||||
|
if not self.cut_extend:
|
||||||
|
self.cut_modules = []
|
||||||
|
self.log.info('start cut modules')
|
||||||
|
self.cut_modules.append(module)
|
||||||
|
self.cut_extend = True
|
||||||
|
if self.version_view:
|
||||||
|
text = 'copied'
|
||||||
|
if not self.set_focus(self.focus + 1):
|
||||||
|
if self.cut_modules[-1] == module:
|
||||||
|
self.cut_modules.pop()
|
||||||
|
else:
|
||||||
|
text = 'cut'
|
||||||
|
self.status(f'{self.describe_buffer()} {text}')
|
||||||
|
|
||||||
|
def insert_cut(self):
|
||||||
|
if self.cut_modules:
|
||||||
|
self.widgets[self.focus:self.focus] = self.cut_modules
|
||||||
|
self.cut_modules = []
|
||||||
|
|
||||||
|
def set_node_name(self, name, cfgpath=None):
|
||||||
|
if name == self.cfgname:
|
||||||
|
return
|
||||||
|
if not name:
|
||||||
|
raise ValueError(f'{name!r} is not a valid node name')
|
||||||
|
self.write_pidfile(name)
|
||||||
|
self.cfgname = name
|
||||||
|
self.titlebar.mid = name
|
||||||
|
versions_path = self.version_dir / f'{name}.versions'
|
||||||
|
try:
|
||||||
|
sections = versions_path.read_text().split(VERSION_SEPARATOR)
|
||||||
|
assert sections.pop(0) == ''
|
||||||
|
except FileNotFoundError:
|
||||||
|
sections = []
|
||||||
|
self.versions = dict((v.split('\n', 1) for v in sections))
|
||||||
|
# the path every change is written to:
|
||||||
|
self.tmppath = self.version_dir / f'{name}.current'
|
||||||
|
try:
|
||||||
|
# if a file exists already, add it to the version history
|
||||||
|
filecontent = self.tmppath.read_text()
|
||||||
|
self.add_version(filecontent, get_timestamp(self.tmppath))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
if cfgpath:
|
||||||
|
cfgpaths = [cfgpath]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
cfgpaths = [to_config_path(name, self.log)]
|
||||||
|
except ConfigError:
|
||||||
|
cfgpaths = []
|
||||||
|
cfgpaths.append(self.tmppath)
|
||||||
|
for cfgpath in cfgpaths:
|
||||||
|
try:
|
||||||
|
filecontent = cfgpath.read_text()
|
||||||
|
self.cfgpath = cfgpath
|
||||||
|
if cfgpath != self.tmppath:
|
||||||
|
self.titlebar.mid = str(cfgpath)
|
||||||
|
timestamp = get_timestamp(cfgpath)
|
||||||
|
break
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
filecontent = None
|
||||||
|
timestamp = time.strftime(TIMESTAMP_FMT)
|
||||||
|
self.cfgpath = cfgpaths[0]
|
||||||
|
self.filecontent = filecontent
|
||||||
|
self.add_version(filecontent, timestamp)
|
||||||
|
|
||||||
|
def add_version(self, filecontent, timestamp):
|
||||||
|
if self.versions:
|
||||||
|
to_remove = []
|
||||||
|
# remove matching versions
|
||||||
|
for key, content in self.versions.items():
|
||||||
|
if content == filecontent:
|
||||||
|
to_remove.append(key)
|
||||||
|
for key in to_remove:
|
||||||
|
self.versions.pop(key)
|
||||||
|
if filecontent:
|
||||||
|
self.versions[timestamp] = filecontent
|
||||||
|
sep = VERSION_SEPARATOR
|
||||||
|
versions_path = self.version_dir / f'{self.cfgname}.versions'
|
||||||
|
tmpname = versions_path.with_suffix('.tmp')
|
||||||
|
with open(tmpname, 'w') as f:
|
||||||
|
for ts, section in self.versions.items():
|
||||||
|
f.write(sep)
|
||||||
|
f.write(f'{ts}\n')
|
||||||
|
f.write(section)
|
||||||
|
os.rename(tmpname, versions_path)
|
||||||
|
|
||||||
|
def restore_version(self):
|
||||||
|
if not self.version_view:
|
||||||
|
self.status('this is already the current version')
|
||||||
|
return
|
||||||
|
self.popupmenu = menu = tg.ConfirmDialog('restore this version? [N]')
|
||||||
|
if not menu.handle(self):
|
||||||
|
self.status('cancelled restore')
|
||||||
|
return
|
||||||
|
version = list(self.versions)[-self.version_view]
|
||||||
|
self.status(f'restored from {version}')
|
||||||
|
content = self.versions.pop(version)
|
||||||
|
self.add_version(self.filecontent, time.strftime(TIMESTAMP_FMT))
|
||||||
|
self.filecontent = content
|
||||||
|
self.version_view = 0
|
||||||
|
self.titlebar.right = ''
|
||||||
|
self.init_from_content(self.filecontent)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_version(self):
|
||||||
|
if self.version_view:
|
||||||
|
version = list(self.versions)[-self.version_view]
|
||||||
|
self.titlebar.right = f'version {version}'
|
||||||
|
try:
|
||||||
|
self.init_from_content(self.versions[version])
|
||||||
|
self.status(None)
|
||||||
|
except Exception as e:
|
||||||
|
self.status('', f'bad version: {e}')
|
||||||
|
else:
|
||||||
|
self.init_from_content(self.filecontent)
|
||||||
|
self.titlebar.right = ''
|
||||||
|
|
||||||
|
def prev_version(self):
|
||||||
|
maxv = len(self.versions)
|
||||||
|
self.version_view += 1
|
||||||
|
self.log.info('back to version %r', self.version_view)
|
||||||
|
if self.version_view > maxv:
|
||||||
|
self.status('this is the oldest version')
|
||||||
|
self.version_view = maxv
|
||||||
|
else:
|
||||||
|
self.set_version()
|
||||||
|
|
||||||
|
def next_version(self):
|
||||||
|
if self.version_view:
|
||||||
|
self.version_view -= 1
|
||||||
|
self.set_version()
|
||||||
|
else:
|
||||||
|
self.status('this is the current version')
|
||||||
|
|
||||||
|
def current_row(self):
|
||||||
|
return super().current_row() + self.get_topmargin()
|
||||||
|
|
||||||
|
def touch(self):
|
||||||
|
self.dirty = True
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
cfgdata = {}
|
||||||
|
for widget in self.widgets:
|
||||||
|
widget.collect(cfgdata)
|
||||||
|
if 'node' not in cfgdata:
|
||||||
|
raise ValueError(list(cfgdata), len(self.widgets))
|
||||||
|
content = cfgdata_to_py(**cfgdata)
|
||||||
|
# if self.cfgpath:
|
||||||
|
# self.cfgpath.write_text(config_code)
|
||||||
|
self.tmppath.write_text(content)
|
||||||
|
self.filecontent = content
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
self.save()
|
||||||
|
try:
|
||||||
|
filecontent = self.cfgpath.read_text()
|
||||||
|
except FileNotFoundError:
|
||||||
|
filecontent = ''
|
||||||
|
if filecontent == self.filecontent:
|
||||||
|
self.log.info('%s was not changed', self.cfgpath)
|
||||||
|
return True
|
||||||
|
savedialog = SaveDialog(self.cfgpath)
|
||||||
|
if savedialog.execute(self) == KEY.QUIT:
|
||||||
|
filename = savedialog.filename
|
||||||
|
if filename:
|
||||||
|
self.log.info('saved %r to %r', self.cfgname, filename)
|
||||||
|
Path(filename).write_text(self.filecontent)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def advance(self, step):
|
||||||
|
done = super().advance(step)
|
||||||
|
if done:
|
||||||
|
self.get_focus_widget().set_focus(None, step)
|
||||||
|
return done
|
||||||
|
|
||||||
|
def write_pidfile(self, name):
|
||||||
|
pidfile = self.version_dir / f'{name}.pid'
|
||||||
|
mypid = os.getpid()
|
||||||
|
for itry in range(15):
|
||||||
|
try:
|
||||||
|
with open(pidfile, 'x') as f:
|
||||||
|
f.write(str(mypid))
|
||||||
|
if self.pidfile and self.pidfile.exists():
|
||||||
|
self.pidfile.unlink()
|
||||||
|
self.pidfile = pidfile
|
||||||
|
return None
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
pid = int(pidfile.read_text())
|
||||||
|
if pid == mypid:
|
||||||
|
if self.pidfile and self.pidfile != pidfile and self.pidfile.exists():
|
||||||
|
self.pidfile.unlink()
|
||||||
|
return None
|
||||||
|
if pid_exists(pid):
|
||||||
|
raise FileExistsError(f'{name} is already edited by process {pid}')
|
||||||
|
pidfile.unlink()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
time.sleep(itry * 0.01)
|
||||||
|
raise RuntimeError('pidfile error: too many tries')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
super().run()
|
||||||
|
except Exception:
|
||||||
|
print(formatExtendedTraceback())
|
||||||
|
finally:
|
||||||
|
if self.filecontent:
|
||||||
|
# add current content to the version history
|
||||||
|
try:
|
||||||
|
self.add_version(self.filecontent, time.strftime(TIMESTAMP_FMT))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
if self.tmppath and self.tmppath.exists():
|
||||||
|
# no need to keep the temporary file, as it has been added to the versions
|
||||||
|
self.tmppath.unlink()
|
||||||
|
if self.pidfile and self.pidfile.exists():
|
||||||
|
self.pidfile.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# os.environ['FRAPPY_CONFDIR'] = 'cfg:cfg/main:cfg/stick:cfg/addons'
|
||||||
|
generalConfig.init()
|
||||||
|
os.environ['FRAPPY_CONFDIR']
|
||||||
|
EditorMain(sys.argv[1]).run()
|
||||||
175
frappy/tools/completion.py
Normal file
175
frappy/tools/completion.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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 inspect
|
||||||
|
from pathlib import Path
|
||||||
|
from importlib import import_module
|
||||||
|
from frappy.core import Module, Parameter, Property
|
||||||
|
from frappy.datatypes import DataType
|
||||||
|
from frappy.lib.comparestring import compare
|
||||||
|
from frappy.tools.configdata import site, ClassChecker, ModuleClass
|
||||||
|
from frappy.tools.terminalgui import Completion
|
||||||
|
|
||||||
|
|
||||||
|
SKIP_PROPS = {'implementation', 'features',
|
||||||
|
'interface_classes', 'slowinterval', 'omit_unchanged_within', 'original_id'}
|
||||||
|
|
||||||
|
|
||||||
|
def recommended_prs(cls):
|
||||||
|
"""get properties and parameters which are useful in configuration
|
||||||
|
|
||||||
|
returns a dict <pname> of <is mandatory (bool)>
|
||||||
|
"""
|
||||||
|
if isinstance(cls, str):
|
||||||
|
try:
|
||||||
|
cls = ClassCompletion.validate(cls)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for pname, pdict in cls.configurables.items():
|
||||||
|
pr = getattr(cls, pname)
|
||||||
|
if isinstance(pr, Property):
|
||||||
|
if pname not in SKIP_PROPS:
|
||||||
|
result[pname] = pr.mandatory
|
||||||
|
elif isinstance(pr, Parameter):
|
||||||
|
if pr.needscfg:
|
||||||
|
result[pname] = True
|
||||||
|
elif not pr.readonly or hasattr(cls, f'write_{pname}'):
|
||||||
|
result[pname] = False
|
||||||
|
if result.get('uri') is False and result.get('io') is False:
|
||||||
|
result['io'] = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class FrappyModule(str):
|
||||||
|
"""checker for finding subclasses of Module defined in a python module"""
|
||||||
|
def check(self, cls):
|
||||||
|
return isinstance(cls, type) and issubclass(cls, Module) and self.startswith(cls.__module__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_suggested(guess, allowed_keys):
|
||||||
|
"""select and sort values from allowed_keys based on similarity to a given value
|
||||||
|
|
||||||
|
:param guess: given value. when empty, the allowed keys are return in the given order
|
||||||
|
:param allowed_keys: list of values sorted in a given order
|
||||||
|
:return: a list of values
|
||||||
|
"""
|
||||||
|
if len(guess) == 0:
|
||||||
|
return allowed_keys
|
||||||
|
low = guess.lower()
|
||||||
|
if len(guess) == 1:
|
||||||
|
# return only items starting with a single letter
|
||||||
|
return [k for k in allowed_keys if k.lower().startswith(low)]
|
||||||
|
comp = {}
|
||||||
|
# if there are items containing guess, return them only
|
||||||
|
result = [k for k in allowed_keys if low in k.lower()]
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
# calculate similarity
|
||||||
|
for key in allowed_keys:
|
||||||
|
comp[key] = compare(guess, key)
|
||||||
|
comp = sorted(comp.items(), key=lambda t: t[1], reverse=True)
|
||||||
|
scorelimit = 2
|
||||||
|
result = []
|
||||||
|
for i, (key, score) in enumerate(comp):
|
||||||
|
if score < scorelimit:
|
||||||
|
break
|
||||||
|
if i > 2:
|
||||||
|
scorelimit = max(2, score - 0.05)
|
||||||
|
result.append(key)
|
||||||
|
return result or allowed_keys
|
||||||
|
|
||||||
|
|
||||||
|
def class_completion(value):
|
||||||
|
"""analyze class path and return an array of suggestions for
|
||||||
|
the last element not matching"""
|
||||||
|
checker = ClassChecker(value)
|
||||||
|
if not checker.error:
|
||||||
|
return checker.position, []
|
||||||
|
if checker.root is None:
|
||||||
|
sdict = {p: f'{p}.' for p in site.packages}
|
||||||
|
else:
|
||||||
|
sdict = {}
|
||||||
|
if not checker.clsobj:
|
||||||
|
file = checker.pyfile
|
||||||
|
if file.name == '__init__.py':
|
||||||
|
sdict = {p.stem: f'{p.stem}.'
|
||||||
|
for p in sorted(file.parent.glob('*.py'))
|
||||||
|
if p.stem != '__init__'}
|
||||||
|
sdict.update((k, k) for k, v in sorted(inspect.getmembers(
|
||||||
|
checker.root, FrappyModule(checker.modname).check)))
|
||||||
|
found = sdict.get(checker.name, None)
|
||||||
|
if found:
|
||||||
|
selection = [found]
|
||||||
|
# selection = [found] + list(sdict.values())
|
||||||
|
else:
|
||||||
|
selection = list(get_suggested(checker.name, sdict.values()))
|
||||||
|
return checker.position, [checker.name] + selection
|
||||||
|
|
||||||
|
|
||||||
|
class NameCompletion(Completion, DataType):
|
||||||
|
# TODO: make obsolete
|
||||||
|
def __init__(self, callback, get_name_info):
|
||||||
|
self.callback = callback
|
||||||
|
self.get_name_info = get_name_info
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def get_selection(self, param):
|
||||||
|
cls, used_names = self.get_name_info()
|
||||||
|
if param:
|
||||||
|
names = dict(self.cls.configurables)
|
||||||
|
selection = []
|
||||||
|
for name in recommended_prs(self.cls):
|
||||||
|
names.pop(name, None)
|
||||||
|
if name not in used_names:
|
||||||
|
selection.append(name)
|
||||||
|
selection.extend(k for k in names if k not in used_names)
|
||||||
|
else:
|
||||||
|
names = dict(cls.configurables.get(param, {}))
|
||||||
|
selection = [k for k in names if f'{param}.{k}' not in used_names]
|
||||||
|
return selection
|
||||||
|
|
||||||
|
def propose(self, value):
|
||||||
|
"""analyze value to propositions of class path
|
||||||
|
|
||||||
|
returns the length of the valid part and a list of guesses
|
||||||
|
"""
|
||||||
|
param, dot, prop = value.partition('.')
|
||||||
|
if (param and not param.isidentifier()) or (prop and not prop.isidentifier()):
|
||||||
|
return 0, None
|
||||||
|
cls, used_names = self.get_name_info()
|
||||||
|
if dot:
|
||||||
|
print(param, repr(dot), prop, list(cls.configurables.get(param, {})))
|
||||||
|
position = len(param) + 1
|
||||||
|
selection = [k for k in cls.configurables.get(param, {}) if f'{param}.{k}' not in used_names]
|
||||||
|
guess = prop
|
||||||
|
else:
|
||||||
|
position = 0
|
||||||
|
selection = {}
|
||||||
|
for name in recommended_prs(cls):
|
||||||
|
if name not in used_names:
|
||||||
|
selection[name] = 1
|
||||||
|
selection.update({k: 1 for k in cls.configurables if k not in used_names})
|
||||||
|
for k, v in cls.configurables.items():
|
||||||
|
print(k, getattr(v, 'needscfg', ''))
|
||||||
|
guess = param
|
||||||
|
selection = get_suggested(guess, selection)
|
||||||
|
return position, [guess] + selection
|
||||||
499
frappy/tools/configdata.py
Normal file
499
frappy/tools/configdata.py
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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 re
|
||||||
|
import frappy
|
||||||
|
from pathlib import Path
|
||||||
|
from ast import literal_eval
|
||||||
|
from importlib import import_module
|
||||||
|
from frappy.config import process_file, Node, fix_io_modules
|
||||||
|
from frappy.core import Module
|
||||||
|
from frappy.datatypes import DataType
|
||||||
|
|
||||||
|
|
||||||
|
HEADER = "# please edit this file with frappy edit"
|
||||||
|
|
||||||
|
|
||||||
|
class Site:
|
||||||
|
domain = 'psi.ch'
|
||||||
|
frappy_subdir = 'frappy_psi'
|
||||||
|
base = Path(frappy.__file__).parent.parent
|
||||||
|
|
||||||
|
def __init__(self, domain='psi.ch', frappy_subdir='frappy_psi', default_interface='tcp://10767'):
|
||||||
|
self.init(domain, frappy_subdir, default_interface)
|
||||||
|
|
||||||
|
def init(self, domain=None, frappy_subdir=None, default_interface=None):
|
||||||
|
if domain:
|
||||||
|
self.domain = domain
|
||||||
|
if default_interface:
|
||||||
|
self.default_interface = default_interface
|
||||||
|
if frappy_subdir:
|
||||||
|
self.packages = [v.name for v in self.base.glob('frappy_*')]
|
||||||
|
try: # psi should be first
|
||||||
|
self.packages.remove(frappy_subdir)
|
||||||
|
self.packages.insert(0, frappy_subdir)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
site = Site()
|
||||||
|
|
||||||
|
|
||||||
|
class NonStringType:
|
||||||
|
"""any type except string"""
|
||||||
|
def validate(self, value):
|
||||||
|
self(value)
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def from_string(self, strvalue):
|
||||||
|
"""convert from string """
|
||||||
|
try:
|
||||||
|
return literal_eval(strvalue)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError('this is no python value')
|
||||||
|
|
||||||
|
def to_string(self, value):
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
def format_value(self, value, unit=None):
|
||||||
|
"""convert to python code (inverse of ast.literal_eval)
|
||||||
|
|
||||||
|
:param value: the value
|
||||||
|
:param unit: must be False (needed for compatibility with frappy.datatypes.DataType)
|
||||||
|
:return: version to used as python code
|
||||||
|
"""
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleStringType(NonStringType):
|
||||||
|
def validate(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def from_string(self, strvalue):
|
||||||
|
"""convert from string """
|
||||||
|
return strvalue
|
||||||
|
|
||||||
|
def to_string(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def format_value(self, value, unit=None):
|
||||||
|
"""convert to string
|
||||||
|
|
||||||
|
:param value:
|
||||||
|
:param unit: must be False (needed for compatibility with frappy datatypes
|
||||||
|
:return: stringified version (triple quoted when containing line breaks)
|
||||||
|
"""
|
||||||
|
if '\n' in value:
|
||||||
|
value = value.replace('"""', '\\"\\"\\"')
|
||||||
|
return f'"""{value}"""'
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
|
||||||
|
nonstringtype = NonStringType()
|
||||||
|
stringtype = SimpleStringType()
|
||||||
|
|
||||||
|
|
||||||
|
class Value:
|
||||||
|
"""a value with additional info
|
||||||
|
|
||||||
|
- hold a value (self.value) and a stringified value self.strvalue
|
||||||
|
- set from and get a string representation for use in a input element
|
||||||
|
- get a python code representation (to be reverted with ast.literal_eval)
|
||||||
|
- verify if the datatype is valid (this typically needs extension)
|
||||||
|
- get information for completion
|
||||||
|
"""
|
||||||
|
error = None
|
||||||
|
strvalue = None
|
||||||
|
modulecls = None
|
||||||
|
datatype = None
|
||||||
|
value = None
|
||||||
|
completion = None
|
||||||
|
|
||||||
|
def __init__(self, value, datatype=None, error=None, from_string=False, callback=None):
|
||||||
|
if value is None:
|
||||||
|
raise ValueError(datatype)
|
||||||
|
self.datatype = datatype
|
||||||
|
self.error = error
|
||||||
|
if callback:
|
||||||
|
self.callback = callback
|
||||||
|
if from_string:
|
||||||
|
if datatype is None:
|
||||||
|
try:
|
||||||
|
literal_eval(value)
|
||||||
|
self.datatype = nonstringtype
|
||||||
|
except Exception:
|
||||||
|
self.datatype = stringtype
|
||||||
|
self.set_from_string(value)
|
||||||
|
else:
|
||||||
|
if datatype is None:
|
||||||
|
self.datatype = stringtype if isinstance(value, str) else nonstringtype
|
||||||
|
self.set_value(value)
|
||||||
|
|
||||||
|
def callback(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
self.strvalue = None
|
||||||
|
try:
|
||||||
|
dt = self.datatype
|
||||||
|
value = dt(value)
|
||||||
|
self.strvalue = dt.to_string(value)
|
||||||
|
dt.validate(value)
|
||||||
|
self.value = self.callback(value)
|
||||||
|
except Exception as e:
|
||||||
|
self.value = value
|
||||||
|
self.error = repr(e)
|
||||||
|
if self.strvalue is None:
|
||||||
|
self.strvalue = str(value)
|
||||||
|
|
||||||
|
def validate_from_string(self, strvalue):
|
||||||
|
self.strvalue = strvalue
|
||||||
|
self.value = self.callback(self.datatype.from_string(strvalue))
|
||||||
|
|
||||||
|
def set_from_string(self, strvalue):
|
||||||
|
try:
|
||||||
|
self.validate_from_string(strvalue)
|
||||||
|
except Exception as e:
|
||||||
|
self.error = repr(e)
|
||||||
|
|
||||||
|
def get_repr(self):
|
||||||
|
"""convert string value to repr
|
||||||
|
|
||||||
|
:return: repr, to be used when building config code
|
||||||
|
"""
|
||||||
|
if self.datatype:
|
||||||
|
try:
|
||||||
|
return self.datatype.format_value(self.value, False)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
return repr(self.strvalue)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'{type(self).__name__}({self.value!r}, {self.datatype!r})'
|
||||||
|
|
||||||
|
|
||||||
|
def get_datatype(pname, cls, value):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param pname: <property name> or <parameter name> or <parametger name>.<property name>
|
||||||
|
:param cls: a frappy Module class or None
|
||||||
|
:param value: the given value (needed only in case the datatype can not be determined)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
param, _, prop = pname.partition('.')
|
||||||
|
error = None
|
||||||
|
if cls:
|
||||||
|
try:
|
||||||
|
prop_param = cls.configurables[param]
|
||||||
|
if isinstance(prop_param, dict):
|
||||||
|
propobj = prop_param.get(prop) if prop else cls.accessibles.get(param)
|
||||||
|
if propobj is None:
|
||||||
|
error = f'{cls.__module__}.{cls.__qualname__}.{param}.{prop} is not configurable'
|
||||||
|
else:
|
||||||
|
return propobj.datatype, None
|
||||||
|
elif prop:
|
||||||
|
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not a parameter'
|
||||||
|
else:
|
||||||
|
return prop_param.datatype, None
|
||||||
|
except AttributeError:
|
||||||
|
error = f'{cls.__module__}.{cls.__qualname__} is not a Frappy Module'
|
||||||
|
except KeyError:
|
||||||
|
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not configurable'
|
||||||
|
if isinstance(value, str):
|
||||||
|
return stringtype, error
|
||||||
|
return nonstringtype, error
|
||||||
|
|
||||||
|
|
||||||
|
def make_value(pname, cls, value):
|
||||||
|
"""make value object"""
|
||||||
|
return Value(value, *get_datatype(pname, cls, value))
|
||||||
|
|
||||||
|
|
||||||
|
class ClassChecker:
|
||||||
|
root = None # = clsobj if the imported object exists or modobj
|
||||||
|
modobj = None # the python module imported or None if no import succeeded
|
||||||
|
clsobj = None # the object imported or None on failure
|
||||||
|
modname = None # the name of the module imported
|
||||||
|
pyfile = None # the python file of the module imported
|
||||||
|
error = None # None on success or a reason of failure
|
||||||
|
|
||||||
|
def __init__(self, clsstr):
|
||||||
|
"""analyze clsstr
|
||||||
|
|
||||||
|
try to resolve clsstr from left to right, quit on error
|
||||||
|
overwrite the class attributes above
|
||||||
|
"""
|
||||||
|
clspath = clsstr.split('.')
|
||||||
|
for pathpos, name in enumerate(clspath):
|
||||||
|
base = '.'.join(clspath[:pathpos])
|
||||||
|
if name:
|
||||||
|
if self.clsobj:
|
||||||
|
error = self.try_cls(base, name)
|
||||||
|
elif name.isupper():
|
||||||
|
error = self.try_cls(base, name)
|
||||||
|
if error and self.try_module(base, name) is None:
|
||||||
|
error = None
|
||||||
|
else:
|
||||||
|
error = self.try_module(base, name)
|
||||||
|
if error and self.try_cls(base, name) is None:
|
||||||
|
error = None
|
||||||
|
else:
|
||||||
|
error = 'empty element'
|
||||||
|
if error:
|
||||||
|
self.name = name
|
||||||
|
self.error = error
|
||||||
|
self.position = sum(len(v) for v in clspath[:pathpos]) + pathpos
|
||||||
|
return
|
||||||
|
self.root = self.clsobj or self.modobj
|
||||||
|
self.name = None
|
||||||
|
self.error = None
|
||||||
|
self.position = len(clsstr)
|
||||||
|
|
||||||
|
def try_module(self, base, name):
|
||||||
|
"""try if base + name is a python module
|
||||||
|
|
||||||
|
return None on success or an error message otherwise
|
||||||
|
"""
|
||||||
|
modname = f'{base}.{name}' if base else name
|
||||||
|
try:
|
||||||
|
self.modobj = self.root = import_module(modname)
|
||||||
|
self.modname = modname
|
||||||
|
self.pyfile = Path(self.modobj.__file__)
|
||||||
|
return None
|
||||||
|
except ImportError as e:
|
||||||
|
return str(e)
|
||||||
|
except Exception as e:
|
||||||
|
return f'{modname}: {e!r}'
|
||||||
|
|
||||||
|
def try_cls(self, base, name):
|
||||||
|
"""try if base + name is a python object (typically a class)
|
||||||
|
|
||||||
|
return None on success or an error message otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.clsobj = getattr(self.root, name)
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return f'{base}.{name} does not exist'
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleClass(DataType):
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, value, previous=None):
|
||||||
|
if isinstance(value, type):
|
||||||
|
if issubclass(value, Module):
|
||||||
|
return value
|
||||||
|
raise ValueError('value is a class, but not a frappy module')
|
||||||
|
checker = ClassChecker(value)
|
||||||
|
if checker.error:
|
||||||
|
raise ValueError(checker.error)
|
||||||
|
if checker.clsobj is None:
|
||||||
|
raise ValueError(value)
|
||||||
|
return checker.clsobj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, strvalue):
|
||||||
|
return cls.validate(strvalue)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_string(cls, value):
|
||||||
|
value = cls.validate(value)
|
||||||
|
return f'{value.__module__}.{value.__qualname__}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def format_value(cls, value, unit=None):
|
||||||
|
result = repr(cls.to_string(value))
|
||||||
|
if '<' in result:
|
||||||
|
raise ValueError(result, value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
module_class = ModuleClass()
|
||||||
|
|
||||||
|
|
||||||
|
def moddata_from_cfgfile(name, cls, **kwds):
|
||||||
|
if isinstance(cls, str):
|
||||||
|
clsvalue = Value(cls, module_class, None, from_string=True)
|
||||||
|
else:
|
||||||
|
clsvalue = Value(cls, module_class, None)
|
||||||
|
cls = None if clsvalue.error else clsvalue.value
|
||||||
|
result = {
|
||||||
|
'name': make_value('name', None, name),
|
||||||
|
'cls': clsvalue,
|
||||||
|
}
|
||||||
|
for param, cfgvalue in kwds.items():
|
||||||
|
if isinstance(cfgvalue, dict):
|
||||||
|
for prop, value in cfgvalue.items():
|
||||||
|
pname = param if prop == 'value' else f'{param}.{prop}'
|
||||||
|
result[pname] = make_value(pname, cls, value)
|
||||||
|
else:
|
||||||
|
result[param] = make_value(param, cls, cfgvalue)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def moddata_to_py(name, cls, description, **kwds):
|
||||||
|
if cls.strvalue == '<auto>':
|
||||||
|
uri = kwds.pop('uri')
|
||||||
|
items = [f'IO({name!r}, {uri.strvalue!r}']
|
||||||
|
if description.strvalue:
|
||||||
|
items.append(f'description={description.strvalue!r}')
|
||||||
|
else:
|
||||||
|
if '<' in cls.get_repr():
|
||||||
|
raise ValueError(cls)
|
||||||
|
items = [f'Mod({name!r}', cls.get_repr(), description.get_repr()]
|
||||||
|
paramdict = {}
|
||||||
|
for name, valobj in kwds.items():
|
||||||
|
param, _, prop = name.partition('.')
|
||||||
|
paramdict.setdefault(param, {})[prop or 'value'] = valobj
|
||||||
|
for name, props in paramdict.items():
|
||||||
|
valueitem = props.pop('value', None)
|
||||||
|
if valueitem is None:
|
||||||
|
args = []
|
||||||
|
else:
|
||||||
|
args = [valueitem.get_repr()]
|
||||||
|
if not props:
|
||||||
|
# single value
|
||||||
|
items.append(f'{name} = {args[0]}')
|
||||||
|
continue
|
||||||
|
# args contains value
|
||||||
|
# extend with keyworded values for parameter properties
|
||||||
|
args.extend(f'{k}={v.get_repr()}' for k, v in props.items())
|
||||||
|
items.append(f"{name} = Param({', '.join(args)})")
|
||||||
|
if len(items) == 1:
|
||||||
|
return f"{items[0]})"
|
||||||
|
items.append(')')
|
||||||
|
return ',\n '.join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_equipment_id(name, equipment_id):
|
||||||
|
"""normalize equipment id"""
|
||||||
|
if re.match(r'[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$', equipment_id):
|
||||||
|
return equipment_id
|
||||||
|
return f'{name}.{site.domain}'
|
||||||
|
|
||||||
|
|
||||||
|
def fix_node_class(cls):
|
||||||
|
if cls == Node('', '')['cls']:
|
||||||
|
return ''
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
def nodedata_from_cfgfile(name, equipment_id='', description='', interface='', cls='', **kwds):
|
||||||
|
title, _, doc = description.partition('\n')
|
||||||
|
if doc.startswith('\n'):
|
||||||
|
doc = doc[1:]
|
||||||
|
props = {
|
||||||
|
'name': name,
|
||||||
|
'title': title,
|
||||||
|
'doc': doc,
|
||||||
|
}
|
||||||
|
eq = fix_equipment_id(name, equipment_id)
|
||||||
|
if eq != fix_equipment_id(name, ''):
|
||||||
|
props['equipment_id'] = eq
|
||||||
|
if interface and interface != site.default_interface:
|
||||||
|
props['interface'] = interface
|
||||||
|
cls = fix_node_class(cls)
|
||||||
|
if cls:
|
||||||
|
props['cls'] = cls
|
||||||
|
props.update(kwds)
|
||||||
|
# TODO: do we have to check the proper datatype for node properties?
|
||||||
|
return {k: Value(v) for k, v in props.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def nodedata_to_py(name, title, doc, equipment_id=None, interface=None, cls=None, **kwds):
|
||||||
|
eq_id = fix_equipment_id(name.value, equipment_id.value if equipment_id else '')
|
||||||
|
intfc = site.default_interface if interface is None else interface.value
|
||||||
|
desc = title.value.strip()
|
||||||
|
doc = doc.value.strip()
|
||||||
|
if doc:
|
||||||
|
desc = f'{desc}\n\n{doc}\n'
|
||||||
|
items = [f"doc={Value(desc).get_repr()}\nNode({eq_id!r}, doc", f'interface={intfc!r}']
|
||||||
|
if cls:
|
||||||
|
clsstr = fix_node_class(cls.value)
|
||||||
|
if clsstr:
|
||||||
|
items.append(f'cls={clsstr!r}')
|
||||||
|
for key, value in kwds.items():
|
||||||
|
items.append(f'{key} = {value.get_repr()}')
|
||||||
|
items.append(')')
|
||||||
|
return ',\n '.join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def cfgdata_to_py(node, **moddata):
|
||||||
|
"""convert cfgdata to python code
|
||||||
|
|
||||||
|
:param node: dict <key> of <value object>
|
||||||
|
:param moddata: dict <module name> of dict <key> of <value object>
|
||||||
|
:return: python code
|
||||||
|
"""
|
||||||
|
items = [HEADER, nodedata_to_py(**node)] + [moddata_to_py(k, **v) for k, v in moddata.items()]
|
||||||
|
return '\n\n'.join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def cfgdata_from_py(name, cfgpath, filecontent, logger):
|
||||||
|
if filecontent:
|
||||||
|
config = process_file(cfgpath, logger, filecontent)
|
||||||
|
iodict = {k: v for k, v in config.items() if v.get('cls') == '<auto>'}
|
||||||
|
nodecfg = config.pop('node', {})
|
||||||
|
nodedesc = nodecfg.get('description', '')
|
||||||
|
if not filecontent.startswith(HEADER) and '\n' not in nodedesc:
|
||||||
|
nodecfg['description'] = f'{nodedesc}\n\nafter conversion with frappy edit'
|
||||||
|
else:
|
||||||
|
config = {}
|
||||||
|
iodict = {}
|
||||||
|
nodecfg = {}
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
for modname, modcfg in config.items():
|
||||||
|
modio = modcfg.get('io')
|
||||||
|
if modio: # convert legacy io Mod cfg to IO
|
||||||
|
ioclass = None
|
||||||
|
try:
|
||||||
|
ioname = modio['value']
|
||||||
|
if ioname in iodict:
|
||||||
|
continue
|
||||||
|
iomodcfg = config[ioname]
|
||||||
|
if set(iomodcfg) - {'uri', 'cls', 'description'}:
|
||||||
|
continue
|
||||||
|
iomodcls = iomodcfg['cls']
|
||||||
|
modcls = modcfg['cls']
|
||||||
|
ioclass = f"{modcls}.ioClass"
|
||||||
|
if ModuleClass.validate(iomodcls) != ModuleClass.validate(ioclass):
|
||||||
|
continue
|
||||||
|
iomodcfg['cls'] = '<auto>'
|
||||||
|
iomodcfg.pop('description', None)
|
||||||
|
iodict[ioname] = iomodcfg
|
||||||
|
except Exception as e:
|
||||||
|
if ioclass:
|
||||||
|
iomod, iocls = iomodcls.rsplit('.', 1)
|
||||||
|
mod, cls = modcls.rsplit('.', 1)
|
||||||
|
if mod == iomod:
|
||||||
|
iomodcls = iocls
|
||||||
|
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
|
||||||
|
else:
|
||||||
|
logger.info('error %r when checking io for %r', e, modname)
|
||||||
|
|
||||||
|
modules = {k: moddata_from_cfgfile(k, **v) for k, v in config.items()}
|
||||||
|
for error in errors.values():
|
||||||
|
logger.info(error)
|
||||||
|
return nodedata_from_cfgfile(name, **nodecfg), modules
|
||||||
106
frappy/tools/editorutils.py
Normal file
106
frappy/tools/editorutils.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""helper functions for configuration editor"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from frappy.config import Node
|
||||||
|
from frappy.lib import get_class
|
||||||
|
|
||||||
|
SITE_TAIL = 'psi.ch'
|
||||||
|
|
||||||
|
|
||||||
|
def repr_param(value=object, **kwds):
|
||||||
|
if value is object:
|
||||||
|
if not kwds:
|
||||||
|
return 'Param()'
|
||||||
|
items = []
|
||||||
|
else:
|
||||||
|
if not kwds:
|
||||||
|
return value
|
||||||
|
items = [value]
|
||||||
|
items.extend(f'{k}={v}' for k, v in kwds.items())
|
||||||
|
return f"Param({', '.join(items)})"
|
||||||
|
|
||||||
|
|
||||||
|
def repr_module(modcfg, name, cls):
|
||||||
|
# items = [f'Mod({name}', f'cls={cls}', f'description={repr_param(**description)}'] + [
|
||||||
|
# f'{k}={repr_param(**v)}' for k, v in kwds.items()] + [')']
|
||||||
|
description = modcfg.pop('description', '')
|
||||||
|
items = [f'Mod({name}', cls, repr_param(**description)] + [
|
||||||
|
f'{k}={repr_param(**v)}' for k, v in modcfg.items()] + [')']
|
||||||
|
return ',\n '.join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def repr_node(name, description, cls=None, equipment_id='', **kwds):
|
||||||
|
equipment_id = fix_equipment_id(name, equipment_id)
|
||||||
|
items = [f'Node({equipment_id!r}', f'description={description}']
|
||||||
|
add_node_class(cls, kwds)
|
||||||
|
items.extend(f'{k}={v}' for k, v in kwds.items())
|
||||||
|
items.append(')')
|
||||||
|
return ',\n '.join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_equipment_id(name, equipment_id):
|
||||||
|
if not re.match(r'[a-zA_Z0-9_]+(\.[a-zA_Z0-9_]+)*$', equipment_id):
|
||||||
|
equipment_id = f'{name}.{SITE_TAIL}'
|
||||||
|
return equipment_id
|
||||||
|
|
||||||
|
|
||||||
|
def add_node_class(cls, result):
|
||||||
|
if cls != Node('', '')['cls']:
|
||||||
|
result['cls'] = cls
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_node(name, equipment_id='', description='', cls=None, interface=None, **kwds):
|
||||||
|
result = {}
|
||||||
|
eq = fix_equipment_id(name, equipment_id)
|
||||||
|
if equipment_id and eq != equipment_id:
|
||||||
|
result['equipment_id'] = eq
|
||||||
|
result['description'] = description
|
||||||
|
result['interface'] = interface or 'tcp://5555'
|
||||||
|
add_node_class(cls, result)
|
||||||
|
result.update(kwds)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def convert_modcfg(cfgdict):
|
||||||
|
"""convert cfgdict
|
||||||
|
|
||||||
|
convert parameter properties to individual items <paramname>.<propname>
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for key, cfgvalue in cfgdict.items():
|
||||||
|
if isinstance(cfgvalue, dict):
|
||||||
|
result.update((key, v) if k == 'value' else (f'{key}.{k}', v)
|
||||||
|
for k, v in cfgvalue.items())
|
||||||
|
else:
|
||||||
|
result[key] = cfgvalue
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def needed_properties(cls):
|
||||||
|
if isinstance(cls, str):
|
||||||
|
cls = get_class(cls)
|
||||||
|
result = []
|
||||||
|
for pname, prop in cls.propertyDict.items():
|
||||||
|
if prop.mandatory and pname not in {'implementation', 'features'}:
|
||||||
|
result.append(pname)
|
||||||
|
return result
|
||||||
1401
frappy/tools/terminalgui.py
Normal file
1401
frappy/tools/terminalgui.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ class LakeshoreIO(StringIO):
|
|||||||
|
|
||||||
class TemperatureSensor(HasIO, Readable):
|
class TemperatureSensor(HasIO, Readable):
|
||||||
"""a temperature sensor (generic for different models)"""
|
"""a temperature sensor (generic for different models)"""
|
||||||
|
ioClass = LakeshoreIO
|
||||||
# internal property to configure the channel
|
# internal property to configure the channel
|
||||||
channel = Property('the Lakeshore channel', datatype=StringType())
|
channel = Property('the Lakeshore channel', datatype=StringType())
|
||||||
# 0, 1500 is the allowed range by the LakeShore controller
|
# 0, 1500 is the allowed range by the LakeShore controller
|
||||||
@@ -66,10 +67,11 @@ class TemperatureSensor(HasIO, Readable):
|
|||||||
|
|
||||||
|
|
||||||
class TemperatureLoop(TemperatureSensor, Drivable):
|
class TemperatureLoop(TemperatureSensor, Drivable):
|
||||||
|
ioClass = LakeshoreIO
|
||||||
# lakeshore loop number to be used for this module
|
# lakeshore loop number to be used for this module
|
||||||
loop = Property('lakeshore loop', IntRange(1, 2), default=1)
|
loop = Property('lakeshore loop', IntRange(1, 2), default=1)
|
||||||
target = Parameter(datatype=FloatRange(unit='K', min=0, max=1500))
|
target = Parameter(datatype=FloatRange(unit='K', min=0, max=1500))
|
||||||
heater_range = Property('heater power range', IntRange(0, 3), readonly=False)
|
heater_range = Parameter('heater power range', IntRange(0, 3), readonly=False)
|
||||||
tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly=False)
|
tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly=False)
|
||||||
_driving = False
|
_driving = False
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ class TemperatureLoop(TemperatureSensor, Drivable):
|
|||||||
|
|
||||||
class TemperatureLoop340(TemperatureLoop):
|
class TemperatureLoop340(TemperatureLoop):
|
||||||
# slightly different behaviour for model 340
|
# slightly different behaviour for model 340
|
||||||
heater_range = Property('heater power range', IntRange(0, 5))
|
heater_range = Parameter('heater power range', IntRange(0, 5))
|
||||||
|
|
||||||
def write_heater_range(self, value):
|
def write_heater_range(self, value):
|
||||||
self.communicate(f'RANGE {value};RANGE?')
|
self.communicate(f'RANGE {value};RANGE?')
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class SR830_IO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class StanfRes(HasIO, Readable):
|
class StanfRes(HasIO, Readable):
|
||||||
|
ioClass = SR830_IO
|
||||||
def set_par(self, cmd, *args):
|
def set_par(self, cmd, *args):
|
||||||
"""
|
"""
|
||||||
Set parameter.
|
Set parameter.
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class IO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Power(HasIO, Readable):
|
class Power(HasIO, Readable):
|
||||||
|
ioClass = IO
|
||||||
value = Parameter(datatype=FloatRange(0,300,unit='W'))
|
value = Parameter(datatype=FloatRange(0,300,unit='W'))
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
@@ -67,6 +68,7 @@ class Power(HasIO, Readable):
|
|||||||
|
|
||||||
|
|
||||||
class Output(HasIO, HasControlledBy, Writable):
|
class Output(HasIO, HasControlledBy, Writable):
|
||||||
|
ioClass = IO
|
||||||
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
|
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
|
||||||
target = Parameter(datatype=FloatRange(0,100,unit='%'))
|
target = Parameter(datatype=FloatRange(0,100,unit='%'))
|
||||||
p_value = Parameter('?', datatype=FloatRange(0,100,unit='%'), default=0)
|
p_value = Parameter('?', datatype=FloatRange(0,100,unit='%'), default=0)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class BridgeIO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Base(HasIO):
|
class Base(HasIO):
|
||||||
|
ioClass = BridgeIO
|
||||||
port = Property('modules port', IntRange(0, 15))
|
port = Property('modules port', IntRange(0, 15))
|
||||||
|
|
||||||
def communicate(self, command):
|
def communicate(self, command):
|
||||||
|
|||||||
160
frappy_psi/bronkhorst.py
Normal file
160
frappy_psi/bronkhorst.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Anik Stark <anik.stark@psi.ch>
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
"""Bronkhorst flow or pressure regulators. Communication via ProPar protocol.
|
||||||
|
|
||||||
|
write command: :LnAd01PrTpData\r\n
|
||||||
|
read command: :LnAd04CopyPrTp\r\n (Ln = 06)
|
||||||
|
answer: :LnAd02CopyData\r\n
|
||||||
|
|
||||||
|
Ln: number of bytes (hex digits pairs) following
|
||||||
|
Ad: node address (3...120, always a reply if message is sent to node address 128)
|
||||||
|
Copy: values just to be copied by the reply (first and third digit < 8)
|
||||||
|
recommended practice: Use PrTp for Copy
|
||||||
|
Pr: Process number (<80, manual page 24pp)
|
||||||
|
Tp: Type + parameter number. Type: 00 byte, 20 int, 40 long/float, 60 string
|
||||||
|
for strings either 00 (for nul terminated) or the max. number of chars
|
||||||
|
has to be appended to the type
|
||||||
|
Data: length depending on type.
|
||||||
|
|
||||||
|
read command for direct communication: :06800401210120
|
||||||
|
|
||||||
|
Send values on a scale from 0-32000 (0-100%).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from frappy.core import StringIO, HasIO, Readable, Writable, Drivable, Parameter, Property, \
|
||||||
|
FloatRange, BoolType, EnumType, IntRange, IDLE, BUSY
|
||||||
|
from frappy.errors import CommunicationFailedError
|
||||||
|
|
||||||
|
|
||||||
|
class IO(StringIO):
|
||||||
|
end_of_line = '\r\n' # hex: 0D0A
|
||||||
|
addr = 128
|
||||||
|
identification = [(f':07{addr:02X}047163716300', f':10{addr:02X}02716300.*')] # serial number
|
||||||
|
default_settings = {'baudrate': 38400}
|
||||||
|
|
||||||
|
|
||||||
|
def intpar(process, parameter):
|
||||||
|
return '06', f'{process:02X}{parameter|0x20:02X}'
|
||||||
|
|
||||||
|
def longpar(process, parameter):
|
||||||
|
return '08', f'{process:02X}{parameter|0x40:04X}'
|
||||||
|
|
||||||
|
MEASURE = intpar(1, 0)
|
||||||
|
SETPOINT = intpar(1, 1)
|
||||||
|
RAMP = intpar(1, 2)
|
||||||
|
CONTROL = longpar(114, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(HasIO, Readable):
|
||||||
|
|
||||||
|
ioClass = IO
|
||||||
|
|
||||||
|
value = Parameter('pressure', FloatRange())
|
||||||
|
scale = Property('scale factor', FloatRange(), default=1)
|
||||||
|
addr = Property('node adress', IntRange(0, 255), default=128)
|
||||||
|
|
||||||
|
def get_par(self, length, param, scale):
|
||||||
|
reply = self.communicate(f':{length}{self.addr:02X}04{param}{param}')
|
||||||
|
if reply[:11] != f':{length}{self.addr:02X}02{param}':
|
||||||
|
return CommunicationFailedError(f'bad reply: {reply}')
|
||||||
|
val = int(reply[11:14], 16) / 32000 * scale
|
||||||
|
return val
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_par(*MEASURE, self.scale)
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(Sensor, Writable):
|
||||||
|
|
||||||
|
def set_par(self, length, param, scale, value):
|
||||||
|
reply = self.communicate(f':{length}{self.addr:02X}01{param}{round(value/scale):04X}')
|
||||||
|
if reply[:8] != f':04{self.addr:02X}0000':
|
||||||
|
raise CommunicationFailedError(f'bad reply: {reply}')
|
||||||
|
return self.get_par(length, param, scale)
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
return self.get_par(*SETPOINT, self.scale)
|
||||||
|
|
||||||
|
def write_target(self, value):
|
||||||
|
val = value / self.scale * 32000
|
||||||
|
return self.set_par(*SETPOINT, self.scale, val)
|
||||||
|
|
||||||
|
|
||||||
|
class HasRamp(Drivable):
|
||||||
|
|
||||||
|
setpoint = Parameter('running setpoint', FloatRange())
|
||||||
|
ramp_enable = Parameter('enable ramp mode', BoolType())
|
||||||
|
ramp = Parameter('slope of ramp', FloatRange(1e-6, unit='mbar/min'))
|
||||||
|
tolerance = Property('tolerance for target vs. running setpoint', FloatRange(), default=1)
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
# overwrite Controller.read_target() as setpoint is running
|
||||||
|
return self.read_target
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
super().write_target(target)
|
||||||
|
self.status = BUSY, 'ramping'
|
||||||
|
|
||||||
|
def read_setpoint(self):
|
||||||
|
return super().read_target()
|
||||||
|
|
||||||
|
def read_ramp(self):
|
||||||
|
if abs(self.read_setpoint() - self.target) < self.tolerance:
|
||||||
|
self.status = IDLE, ''
|
||||||
|
|
||||||
|
def write_ramp(self, ramp):
|
||||||
|
if self.ramp_enable:
|
||||||
|
time = min(self.scale / ramp, 3000)
|
||||||
|
return self.set_par(*RAMP, (60 / 0.1), time)
|
||||||
|
|
||||||
|
def write_ramp_enable(self, flag):
|
||||||
|
if flag:
|
||||||
|
self.write_ramp(self.ramp)
|
||||||
|
else:
|
||||||
|
self.set_par(*RAMP, (60 / 0.1), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class HasControlMode():
|
||||||
|
|
||||||
|
control_active = Parameter('control mode active', BoolType())
|
||||||
|
control = Property('control mode', EnumType(manual=4, loop=11))
|
||||||
|
output = Parameter('valve output', FloatRange(), readonly=False)
|
||||||
|
|
||||||
|
def write_control(self, value):
|
||||||
|
if self.control_active:
|
||||||
|
val = self.control.get(value, 4)
|
||||||
|
return self.set_par(*CONTROL, 1, val)
|
||||||
|
|
||||||
|
def write_output(self, value):
|
||||||
|
scale = (2**24 - 1) / 100
|
||||||
|
self.set_par(*CONTROL, scale, value)
|
||||||
|
|
||||||
|
|
||||||
|
class ControllerRamp(HasRamp, Controller):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ControllerControlMode(HasControlMode, Controller):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ControllerRampControlMode(HasRamp, HasControlMode, Controller):
|
||||||
|
pass
|
||||||
@@ -1,53 +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>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
|
|
||||||
|
|
||||||
from frappy.core import Writable, BoolType, FloatRange, HasIO, Property, StringType, Parameter
|
|
||||||
from frappy_psi.logo import Sensor
|
|
||||||
|
|
||||||
|
|
||||||
class Heater(Sensor):
|
|
||||||
addr = 'VW10' # heater readback address
|
|
||||||
scale = 0.1
|
|
||||||
switch_addr = Property('address for switch', StringType(), default='V0.1')
|
|
||||||
enable_addr = Property('address for enable', StringType(), default='V0.0')
|
|
||||||
maxheater_addr = Property('address for target', StringType(), default='VW10')
|
|
||||||
value = Parameter(unit='%')
|
|
||||||
switch = Parameter('heater is enabled', BoolType())
|
|
||||||
enable = Parameter('heater enabled', BoolType(), readonly=False)
|
|
||||||
maxheater = Parameter('max. heater power', FloatRange(unit='%'), readonly=False)
|
|
||||||
|
|
||||||
def read_switch(self):
|
|
||||||
return self.get_vm_value(self.switch_addr)
|
|
||||||
|
|
||||||
def read_enable(self):
|
|
||||||
return self.get_vm_value(self.enable_addr)
|
|
||||||
|
|
||||||
def write_enable(self, value):
|
|
||||||
self.set_vm_value(self.enable_addr, value)
|
|
||||||
return self.read_enable()
|
|
||||||
|
|
||||||
def read_maxheater(self):
|
|
||||||
return self.get_vm_value(self.maxheater_addr, self.scale)
|
|
||||||
|
|
||||||
def write_maxheater(self, value):
|
|
||||||
self.io.set_vm_value(self.maxheater_addr, value, self.scale)
|
|
||||||
return self.read_maxheater()
|
|
||||||
@@ -8,7 +8,7 @@ from frappy.errors import ConfigError
|
|||||||
class Rack:
|
class Rack:
|
||||||
configbase = Path('/home/l_samenv/.config/frappy_instruments')
|
configbase = Path('/home/l_samenv/.config/frappy_instruments')
|
||||||
|
|
||||||
def __init__(self, modfactory, **kwds):
|
def __init__(self, modfactory):
|
||||||
self.modfactory = modfactory
|
self.modfactory = modfactory
|
||||||
instpath = self.configbase / os.environ['Instrument']
|
instpath = self.configbase / os.environ['Instrument']
|
||||||
sections = {}
|
sections = {}
|
||||||
|
|||||||
260
frappy_psi/dilution_new.py
Normal file
260
frappy_psi/dilution_new.py
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Andrea Plank <andrea.plank@psi.ch>
|
||||||
|
# Anik Stark <anik.stark@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
from frappy.core import Drivable, Parameter, Attached, FloatRange, \
|
||||||
|
IDLE, BUSY, WARN, ERROR
|
||||||
|
from frappy.datatypes import EnumType, BoolType, StructOf, StringType
|
||||||
|
from frappy.states import Retry, Finish, status_code, HasStates
|
||||||
|
from frappy.lib.enum import Enum
|
||||||
|
from frappy.errors import ImpossibleError
|
||||||
|
from frappy.lib.mathparser import MathParser
|
||||||
|
|
||||||
|
T = Enum( # target states
|
||||||
|
off = 0,
|
||||||
|
sorbpump = 2,
|
||||||
|
condense = 5,
|
||||||
|
remove = 7,
|
||||||
|
remove_and_sorbpump = 9,
|
||||||
|
remove_and_condense = 10,
|
||||||
|
manual = 11,
|
||||||
|
test = 12,
|
||||||
|
)
|
||||||
|
|
||||||
|
V = Enum(T, # value status inherits from target status
|
||||||
|
sorbpumping=1,
|
||||||
|
condensing=4,
|
||||||
|
circulating=6,
|
||||||
|
removing=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Dilution(HasStates, Drivable):
|
||||||
|
|
||||||
|
condenseline_pressure = Attached() # G1
|
||||||
|
condense_valve = Attached() # V1
|
||||||
|
dump_valve = Attached() # V9
|
||||||
|
forepump = Attached() # rotary_pump_He3 (24)
|
||||||
|
condenseline_valve = Attached() # V1
|
||||||
|
circuitshort_valve = Attached() # V3
|
||||||
|
still_valve = Attached() # V6
|
||||||
|
pumpout_valve = Attached() # V14
|
||||||
|
still_pressure = Attached() # P1
|
||||||
|
dump_pressure = Attached() # G3
|
||||||
|
oneK_temp = Attached()
|
||||||
|
still_temp = Attached()
|
||||||
|
mix_temp = Attached()
|
||||||
|
|
||||||
|
value = Parameter('current state', EnumType(V), default=0)
|
||||||
|
target = Parameter('target state', EnumType(T), default=0)
|
||||||
|
sorbpumped = Parameter('sorb pump done', BoolType(), default=False, readonly=False)
|
||||||
|
sorb_cond = Parameter('sorb condition', StringType(), default='oneK>4', readonly=False)
|
||||||
|
sorb_pump_time = Parameter('sorb pump time', FloatRange(), default=2400, readonly=False)
|
||||||
|
dump_target = Parameter('low dump pressure limit indicating end of condensation phase',
|
||||||
|
FloatRange(unit='mbar * min'), readonly=False, default=50)
|
||||||
|
pulse_factor = Parameter('factor for calculating pump out pulse length',
|
||||||
|
FloatRange(unit='mbar'), readonly=False, default=20)
|
||||||
|
end_condense_pressure = Parameter('low condense pressure indicating end of condensation phase',
|
||||||
|
FloatRange(unit='mbar'), readonly=False, default=50)
|
||||||
|
end_remove_pressure = Parameter('pressure reached before end of remove (before fore pump)',
|
||||||
|
FloatRange(unit='mbar'), readonly=False, default=0.02)
|
||||||
|
condensing_p_low = Parameter('when to start pumping dump', datatype=FloatRange(unit='mbar'), default=500, readonly=False)
|
||||||
|
st = StringType()
|
||||||
|
valve_set = StructOf(close=st, open=st, check_open=st, check_closed=st)
|
||||||
|
condense_valves = Parameter('valve to act when condensing', valve_set)
|
||||||
|
valves_after_remove = Parameter('valve to act after remove', valve_set)
|
||||||
|
check_after_remove = Parameter('check for manual valves after remove', valve_set)
|
||||||
|
init = True
|
||||||
|
_start_time = 0
|
||||||
|
_warn_manual_work = None
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
"""
|
||||||
|
if (target == Targetstates.SORBPUMP):
|
||||||
|
if self.value == target:
|
||||||
|
return self.target
|
||||||
|
self.start_machine(self.sorbpump)
|
||||||
|
self.value = Targetstates.SORBPUMP
|
||||||
|
return self.value
|
||||||
|
"""
|
||||||
|
self.log.info('start %s', target.name)
|
||||||
|
if self.value == target:
|
||||||
|
return target
|
||||||
|
try:
|
||||||
|
self.start_machine(getattr(self, target.name, None))
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception('error %s', e)
|
||||||
|
self.log.info('started %s', target.name)
|
||||||
|
return target
|
||||||
|
|
||||||
|
@status_code(BUSY, 'sorbpump state')
|
||||||
|
def sorbpump(self, state):
|
||||||
|
""" heat up to Tsorb and wait """
|
||||||
|
if state.init:
|
||||||
|
#self.ls372.write_target(40) # set Tsorb to 40K
|
||||||
|
self.start_time = state.now
|
||||||
|
self.handle_valves(**self.condense_valves)
|
||||||
|
return Retry
|
||||||
|
parser = MathParser(oneK=self.oneK_temp.value, still=self.still_temp.value, mix=self.mix_temp.value)
|
||||||
|
if parser.calculate(self.sorb_cond):
|
||||||
|
self.start_time = state.now
|
||||||
|
if state.now - self.start_time < self.sorb_pump_time:
|
||||||
|
return Retry
|
||||||
|
return self.condense
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def condense(self, state):
|
||||||
|
""" condense process """
|
||||||
|
if state.init:
|
||||||
|
# self.value = V.condensing
|
||||||
|
self.handle_valves(**self.condense_valves)
|
||||||
|
self.still_valve.write_target(100)
|
||||||
|
self._start_time = state.now
|
||||||
|
return Retry
|
||||||
|
pdump = self.dump_pressure.value
|
||||||
|
pcond = self.condenseline_pressure.read_value()
|
||||||
|
if pcond < self.condensing_p_low and state.now > self._start_time + 5:
|
||||||
|
pulse_time = 60 * self.pulse_factor / pdump
|
||||||
|
if pulse_time > 59:
|
||||||
|
pulse_time = 3600
|
||||||
|
self.pumpout_valve.delay = pulse_time
|
||||||
|
self.pumpout_valve.write_target(1)
|
||||||
|
if pdump > self.dump_target:
|
||||||
|
return Retry
|
||||||
|
return self.wait_for_condense_line_pressure
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def wait_for_condense_line_pressure(self, state):
|
||||||
|
if self.condenseline_pressure.read_value() > self.end_condense_pressure:
|
||||||
|
return Retry
|
||||||
|
self.condense_valve.write_target(0)
|
||||||
|
return self.circulate
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def circulate(self, state):
|
||||||
|
"""Zirkuliert die Mischung."""
|
||||||
|
if state.init:
|
||||||
|
self.handle_valves(**self.condense_valves)
|
||||||
|
if self.wait_valves():
|
||||||
|
return Retry
|
||||||
|
self.check_valve_result()
|
||||||
|
self.value = V.circulating
|
||||||
|
return Finish
|
||||||
|
|
||||||
|
@status_code(BUSY, 'remove (wait for turbo shut down)')
|
||||||
|
def remove(self, state):
|
||||||
|
"""Entfernt die Mischung."""
|
||||||
|
if state.init:
|
||||||
|
self.handle_valves(**self.remove_valves)
|
||||||
|
return Retry
|
||||||
|
self.circuitshort_valve.write_target(1)
|
||||||
|
return self.remove_endsequence
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def remove_endsequence(self, state):
|
||||||
|
if self.still_pressure.read_value() > self.end_remove_pressure:
|
||||||
|
return Retry
|
||||||
|
self.circuitshort_valve.write_target(0)
|
||||||
|
self.dump_valve.write_target(0)
|
||||||
|
return self.close_valves_after_remove
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def close_valves_after_remove(self, state):
|
||||||
|
if state.init:
|
||||||
|
self.handle_valves(**self.valves_after_remove)
|
||||||
|
self._warn_manual_work = True
|
||||||
|
return self.final_status(WARN, 'please check manual valves')
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
status = super().read_status()
|
||||||
|
if status[0] < ERROR and self._warn_manual_work:
|
||||||
|
try:
|
||||||
|
self.handle_valves(**self.check_after_remove)
|
||||||
|
self._warn_manual_work = False
|
||||||
|
except ImpossibleError:
|
||||||
|
return WARN, f'please close manual valves {",".join(self._valves_failed[False])}'
|
||||||
|
return status
|
||||||
|
|
||||||
|
def handle_valves(self, check_closed=(), check_open=(), close=(), open=()):
|
||||||
|
"""check ot set given valves
|
||||||
|
raises ImpossibleError, when checks fails """
|
||||||
|
self._valves_to_wait_for = {}
|
||||||
|
self._valves_failed = {True: [], False: []}
|
||||||
|
for flag, valves in enumerate([check_closed, check_open]):
|
||||||
|
for vname in valves.split():
|
||||||
|
if self.secNode.modules[vname].read_value() != flag:
|
||||||
|
self._valves_failed[flag].append(vname)
|
||||||
|
for flag, valves in enumerate([close, open]):
|
||||||
|
for vname in valves.split():
|
||||||
|
valve = self.secNode.modules[vname]
|
||||||
|
valve.write_target(flag)
|
||||||
|
if valve.isBusy():
|
||||||
|
self._valves_to_wait_for[vname] = (valve, flag)
|
||||||
|
elif valve.read_value() != flag:
|
||||||
|
self._valves_failed[flag].append(vname)
|
||||||
|
|
||||||
|
def wait_valves(self):
|
||||||
|
busy = False
|
||||||
|
for vname, (valve, flag) in dict(self._valves_to_wait_for.items()):
|
||||||
|
statuscode = valve.read_status()[0]
|
||||||
|
if statuscode == BUSY:
|
||||||
|
busy = True
|
||||||
|
continue
|
||||||
|
if valve.read_value() == flag and statuscode == IDLE:
|
||||||
|
self._valves_to_wait_for.pop(vname)
|
||||||
|
else:
|
||||||
|
self._valves_failed[flag].append(vname)
|
||||||
|
return busy
|
||||||
|
|
||||||
|
def check_valve_result(self):
|
||||||
|
result = []
|
||||||
|
for flag, valves in self._valves_failed.items():
|
||||||
|
if valves:
|
||||||
|
result.append(f"{','.join(valves)} not {'open' if flag else 'closed'}")
|
||||||
|
if result:
|
||||||
|
raise ImpossibleError(f"failed: {', '.join(result)}")
|
||||||
|
|
||||||
|
|
||||||
|
class DIL4(Dilution):
|
||||||
|
condense_valves = {
|
||||||
|
'close': 'V2 V3 V4 V7 V8 V10 V11A V12A V13A',
|
||||||
|
'check_closed': '',
|
||||||
|
'check_open': '',
|
||||||
|
'open': 'V1 V5 V9',
|
||||||
|
}
|
||||||
|
remove_valves = {
|
||||||
|
'close': '',
|
||||||
|
'check_closed': '',
|
||||||
|
'check_open': '',
|
||||||
|
'open': '',
|
||||||
|
}
|
||||||
|
valves_after_remove = {
|
||||||
|
'close': '',
|
||||||
|
'check_closed': '',
|
||||||
|
'open': '',
|
||||||
|
'check_open': '',
|
||||||
|
}
|
||||||
|
check_after_remove = {
|
||||||
|
'close': '',
|
||||||
|
'check_closed': '',
|
||||||
|
'open': '',
|
||||||
|
'check_open': '',
|
||||||
|
}
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# *****************************************************************************
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
||||||
# *****************************************************************************
|
|
||||||
"""oxford instruments mercury IPS power supply"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from frappy.core import Parameter, EnumType, FloatRange, BoolType, IntRange, Property, Module
|
|
||||||
from frappy.lib.enum import Enum
|
|
||||||
from frappy.errors import BadValueError, HardwareError
|
|
||||||
from frappy_psi.magfield import Magfield, SimpleMagfield, Status
|
|
||||||
from frappy_psi.mercury import MercuryChannel, off_on, Mapped
|
|
||||||
from frappy.states import Retry
|
|
||||||
|
|
||||||
Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3)
|
|
||||||
hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
|
|
||||||
RTOZ=Action.run_to_zero, CLMP=Action.clamped)
|
|
||||||
CURRENT_CHECK_SIZE = 2
|
|
||||||
|
|
||||||
|
|
||||||
class Field(Magfield):
|
|
||||||
action = Parameter('action', EnumType(Action), readonly=False)
|
|
||||||
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
|
|
||||||
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
|
|
||||||
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
|
|
||||||
working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
|
|
||||||
persistent_field = Parameter(
|
|
||||||
'persistent field at last switch off', FloatRange(unit='$'), readonly=False)
|
|
||||||
wait_switch_on = Parameter(
|
|
||||||
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60)
|
|
||||||
wait_switch_off = Parameter(
|
|
||||||
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=True, default=60)
|
|
||||||
forced_persistent_field = Parameter(
|
|
||||||
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
|
|
||||||
|
|
||||||
_field_mismatch = None
|
|
||||||
__persistent_field = None # internal value of persistent field
|
|
||||||
__switch_fixed_until = 0
|
|
||||||
|
|
||||||
def initModule(self):
|
|
||||||
super().initModule()
|
|
||||||
try:
|
|
||||||
self.write_action(Action.hold)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error('can not set to hold %r', e)
|
|
||||||
|
|
||||||
def doPoll(self):
|
|
||||||
super().doPoll()
|
|
||||||
self.read_current()
|
|
||||||
|
|
||||||
def initialReads(self):
|
|
||||||
# on restart, assume switch is changed long time ago, if not, the mercury
|
|
||||||
# will complain and this will be handled in start_ramp_to_field
|
|
||||||
self.switch_on_time = 0
|
|
||||||
self.switch_off_time = 0
|
|
||||||
self.switch_heater = self.query('DEV::PSU:SIG:SWHT', off_on)
|
|
||||||
super().initialReads()
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
return self.query('DEV::PSU:SIG:FLD')
|
|
||||||
|
|
||||||
def read_ramp(self):
|
|
||||||
return self.query('DEV::PSU:SIG:RFST')
|
|
||||||
|
|
||||||
def write_ramp(self, value):
|
|
||||||
return self.change('DEV::PSU:SIG:RFST', value)
|
|
||||||
|
|
||||||
def read_action(self):
|
|
||||||
return self.query('DEV::PSU:ACTN', hold_rtoz_rtos_clmp)
|
|
||||||
|
|
||||||
def write_action(self, value):
|
|
||||||
return self.change('DEV::PSU:ACTN', value, hold_rtoz_rtos_clmp)
|
|
||||||
|
|
||||||
def read_atob(self):
|
|
||||||
return self.query('DEV::PSU:ATOB')
|
|
||||||
|
|
||||||
def read_voltage(self):
|
|
||||||
return self.query('DEV::PSU:SIG:VOLT')
|
|
||||||
|
|
||||||
def read_working_ramp(self):
|
|
||||||
return self.query('DEV::PSU:SIG:RFLD')
|
|
||||||
|
|
||||||
def read_setpoint(self):
|
|
||||||
return self.query('DEV::PSU:SIG:FSET')
|
|
||||||
|
|
||||||
def set_and_go(self, value):
|
|
||||||
self.setpoint = self.change('DEV::PSU:SIG:FSET', value)
|
|
||||||
assert self.write_action(Action.hold) == Action.hold
|
|
||||||
assert self.write_action(Action.run_to_set) == Action.run_to_set
|
|
||||||
|
|
||||||
def start_ramp_to_target(self, sm):
|
|
||||||
# if self.action != Action.hold:
|
|
||||||
# assert self.write_action(Action.hold) == Action.hold
|
|
||||||
# return Retry
|
|
||||||
self.set_and_go(sm.target)
|
|
||||||
sm.try_cnt = 5
|
|
||||||
return self.ramp_to_target
|
|
||||||
|
|
||||||
def ramp_to_target(self, sm):
|
|
||||||
try:
|
|
||||||
return super().ramp_to_target(sm)
|
|
||||||
except HardwareError:
|
|
||||||
sm.try_cnt -= 1
|
|
||||||
if sm.try_cnt < 0:
|
|
||||||
raise
|
|
||||||
self.set_and_go(sm.target)
|
|
||||||
return Retry
|
|
||||||
|
|
||||||
def final_status(self, *args, **kwds):
|
|
||||||
self.write_action(Action.hold)
|
|
||||||
return super().final_status(*args, **kwds)
|
|
||||||
|
|
||||||
def on_restart(self, sm):
|
|
||||||
self.write_action(Action.hold)
|
|
||||||
return super().on_restart(sm)
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
current = self.query('DEV::PSU:SIG:FLD')
|
|
||||||
if self.switch_heater == self.switch_heater.on:
|
|
||||||
self.__persistent_field = current
|
|
||||||
self.forced_persistent_field = False
|
|
||||||
return current
|
|
||||||
pf = self.query('DEV::PSU:SIG:PFLD')
|
|
||||||
if self.__persistent_field is None:
|
|
||||||
self.__persistent_field = pf
|
|
||||||
self._field_mismatch = False
|
|
||||||
else:
|
|
||||||
self._field_mismatch = abs(self.__persistent_field - pf) > self.tolerance * 10
|
|
||||||
self.persistent_field = self.__persistent_field
|
|
||||||
return self.__persistent_field
|
|
||||||
|
|
||||||
def _check_adr(self, adr):
|
|
||||||
"""avoid complains about bad slot"""
|
|
||||||
if adr.startswith('DEV:PSU.M'):
|
|
||||||
return
|
|
||||||
super()._check_adr(adr)
|
|
||||||
|
|
||||||
def read_current(self):
|
|
||||||
current = self.query('DEV::PSU:SIG:CURR')
|
|
||||||
if self.atob:
|
|
||||||
return current / self.atob
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def write_persistent_field(self, value):
|
|
||||||
if self.forced_persistent_field or abs(self.__persistent_field - value) <= self.tolerance * 10:
|
|
||||||
self._field_mismatch = False
|
|
||||||
self.__persistent_field = value
|
|
||||||
return value
|
|
||||||
raise BadValueError('changing persistent field needs forced_persistent_field=True')
|
|
||||||
|
|
||||||
def write_target(self, target):
|
|
||||||
if self._field_mismatch:
|
|
||||||
self.forced_persistent_field = True
|
|
||||||
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
|
|
||||||
return super().write_target(target)
|
|
||||||
|
|
||||||
def read_switch_heater(self):
|
|
||||||
value = self.query('DEV::PSU:SIG:SWHT', off_on)
|
|
||||||
now = time.time()
|
|
||||||
if value != self.switch_heater:
|
|
||||||
if now < self.__switch_fixed_until:
|
|
||||||
self.log.debug('correct fixed switch time')
|
|
||||||
# probably switch heater was changed, but IPS reply is not yet updated
|
|
||||||
if self.switch_heater:
|
|
||||||
self.switch_on_time = time.time()
|
|
||||||
else:
|
|
||||||
self.switch_off_time = time.time()
|
|
||||||
return self.switch_heater
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_wait_switch_on(self):
|
|
||||||
return self.query('DEV::PSU:SWONT') * 0.001
|
|
||||||
|
|
||||||
def read_wait_switch_off(self):
|
|
||||||
return self.query('DEV::PSU:SWOFT') * 0.001
|
|
||||||
|
|
||||||
def write_switch_heater(self, value):
|
|
||||||
if value == self.read_switch_heater():
|
|
||||||
self.log.info('switch heater already %r', value)
|
|
||||||
# we do not want to restart the timer
|
|
||||||
return value
|
|
||||||
self.__switch_fixed_until = time.time() + 10
|
|
||||||
self.log.debug('switch time fixed for 10 sec')
|
|
||||||
result = self.change('DEV::PSU:SIG:SWHT', value, off_on, n_retry=0) # no readback check
|
|
||||||
return result
|
|
||||||
|
|
||||||
def start_ramp_to_field(self, sm):
|
|
||||||
if abs(self.current - self.__persistent_field) <= self.tolerance:
|
|
||||||
self.log.info('leads %g are already at %g', self.current, self.__persistent_field)
|
|
||||||
return self.ramp_to_field
|
|
||||||
try:
|
|
||||||
self.set_and_go(self.__persistent_field)
|
|
||||||
except (HardwareError, AssertionError) as e:
|
|
||||||
if self.switch_heater:
|
|
||||||
self.log.warn('switch is already on!')
|
|
||||||
return self.ramp_to_field
|
|
||||||
self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.__persistent_field, e)
|
|
||||||
sm.after_wait = self.ramp_to_field
|
|
||||||
return self.wait_for_switch
|
|
||||||
return self.ramp_to_field
|
|
||||||
|
|
||||||
def start_ramp_to_target(self, sm):
|
|
||||||
sm.try_cnt = 5
|
|
||||||
try:
|
|
||||||
self.set_and_go(sm.target)
|
|
||||||
except (HardwareError, AssertionError) as e:
|
|
||||||
self.log.warn('switch not yet ready %r', e)
|
|
||||||
self.status = Status.PREPARING, 'wait for switch on'
|
|
||||||
sm.after_wait = self.ramp_to_target
|
|
||||||
return self.wait_for_switch
|
|
||||||
return self.ramp_to_target
|
|
||||||
|
|
||||||
def ramp_to_field(self, sm):
|
|
||||||
try:
|
|
||||||
return super().ramp_to_field(sm)
|
|
||||||
except HardwareError:
|
|
||||||
sm.try_cnt -= 1
|
|
||||||
if sm.try_cnt < 0:
|
|
||||||
raise
|
|
||||||
self.set_and_go(self.__persistent_field)
|
|
||||||
return Retry
|
|
||||||
|
|
||||||
def wait_for_switch(self, sm):
|
|
||||||
if not sm.delta(10):
|
|
||||||
return Retry
|
|
||||||
try:
|
|
||||||
self.log.warn('try again')
|
|
||||||
# try again
|
|
||||||
self.set_and_go(self.__persistent_field)
|
|
||||||
except (HardwareError, AssertionError):
|
|
||||||
return Retry
|
|
||||||
return sm.after_wait
|
|
||||||
|
|
||||||
def wait_for_switch_on(self, sm):
|
|
||||||
self.read_switch_heater() # trigger switch_on/off_time
|
|
||||||
if self.switch_heater == self.switch_heater.off:
|
|
||||||
if sm.init: # avoid too many states chained
|
|
||||||
return Retry
|
|
||||||
self.log.warning('switch turned off manually?')
|
|
||||||
return self.start_switch_on
|
|
||||||
return super().wait_for_switch_on(sm)
|
|
||||||
|
|
||||||
def wait_for_switch_off(self, sm):
|
|
||||||
self.read_switch_heater()
|
|
||||||
if self.switch_heater == self.switch_heater.on:
|
|
||||||
if sm.init: # avoid too many states chained
|
|
||||||
return Retry
|
|
||||||
self.log.warning('switch turned on manually?')
|
|
||||||
return self.start_switch_off
|
|
||||||
return super().wait_for_switch_off(sm)
|
|
||||||
|
|
||||||
def start_ramp_to_zero(self, sm):
|
|
||||||
pf = self.query('DEV::PSU:SIG:PFLD')
|
|
||||||
if abs(pf - self.value) > self.tolerance * 10:
|
|
||||||
self.log.warning('persistent field %g does not match %g after switch off', pf, self.value)
|
|
||||||
try:
|
|
||||||
assert self.write_action(Action.hold) == Action.hold
|
|
||||||
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
|
||||||
except (HardwareError, AssertionError) as e:
|
|
||||||
self.log.warn('switch not yet ready %r', e)
|
|
||||||
self.status = Status.PREPARING, 'wait for switch off'
|
|
||||||
sm.after_wait = self.ramp_to_zero
|
|
||||||
return self.wait_for_switch
|
|
||||||
return self.ramp_to_zero
|
|
||||||
|
|
||||||
def ramp_to_zero(self, sm):
|
|
||||||
try:
|
|
||||||
return super().ramp_to_zero(sm)
|
|
||||||
except HardwareError:
|
|
||||||
sm.try_cnt -= 1
|
|
||||||
if sm.try_cnt < 0:
|
|
||||||
raise
|
|
||||||
assert self.write_action(Action.hold) == Action.hold
|
|
||||||
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
|
||||||
return Retry
|
|
||||||
@@ -50,6 +50,7 @@ SOURCECMDS = {
|
|||||||
|
|
||||||
|
|
||||||
class SourceMeter(HasIO, Module):
|
class SourceMeter(HasIO, Module):
|
||||||
|
ioClass = K2601bIO
|
||||||
export = False # export for tests only
|
export = False # export for tests only
|
||||||
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||||
readonly=False, export=False)
|
readonly=False, export=False)
|
||||||
@@ -107,6 +108,7 @@ class Resistivity(HasIO, Readable):
|
|||||||
|
|
||||||
|
|
||||||
class Current(HasIO, Writable):
|
class Current(HasIO, Writable):
|
||||||
|
ioClass = K2601bIO
|
||||||
sourcemeter = Attached()
|
sourcemeter = Attached()
|
||||||
|
|
||||||
value = Parameter('measured current', FloatRange(unit='A'))
|
value = Parameter('measured current', FloatRange(unit='A'))
|
||||||
@@ -153,6 +155,7 @@ class Current(HasIO, Writable):
|
|||||||
|
|
||||||
|
|
||||||
class Voltage(HasIO, Writable):
|
class Voltage(HasIO, Writable):
|
||||||
|
ioClass = K2601bIO
|
||||||
sourcemeter = Attached()
|
sourcemeter = Attached()
|
||||||
|
|
||||||
value = Parameter('measured voltage', FloatRange(unit='V'))
|
value = Parameter('measured voltage', FloatRange(unit='V'))
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import time
|
|||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import threading
|
import threading
|
||||||
import numpy as np
|
|
||||||
from numpy.testing import assert_approx_equal
|
|
||||||
|
|
||||||
from frappy.core import Module, Readable, Parameter, Property, \
|
from frappy.core import Module, Readable, Parameter, Property, \
|
||||||
HasIO, StringIO, Writable, IDLE, ERROR, BUSY, DISABLED, nopoll, Attached
|
HasIO, StringIO, Writable, IDLE, ERROR, BUSY, DISABLED, nopoll, Attached
|
||||||
@@ -32,11 +30,14 @@ from frappy.datatypes import IntRange, FloatRange, StringType, \
|
|||||||
from frappy.errors import CommunicationFailedError, ConfigError, \
|
from frappy.errors import CommunicationFailedError, ConfigError, \
|
||||||
HardwareError, DisabledError, ImpossibleError, secop_error, SECoPError
|
HardwareError, DisabledError, ImpossibleError, secop_error, SECoPError
|
||||||
from frappy.lib.units import NumberWithUnit, format_with_unit
|
from frappy.lib.units import NumberWithUnit, format_with_unit
|
||||||
from frappy.lib import formatStatusBits
|
from frappy.lib import formatStatusBits, LazyImport
|
||||||
from frappy_psi.convergence import HasConvergence
|
from frappy_psi.convergence import HasConvergence
|
||||||
from frappy.mixins import HasOutputModule, HasControlledBy
|
from frappy.mixins import HasOutputModule, HasControlledBy
|
||||||
from frappy.extparams import StructParam
|
from frappy.extparams import StructParam
|
||||||
from frappy_psi.calcurve import CalCurve
|
|
||||||
|
np = LazyImport('numpy')
|
||||||
|
np_testing = LazyImport('numpy.testing')
|
||||||
|
calcurve_module = LazyImport('frappy_psi.calcurve')
|
||||||
|
|
||||||
|
|
||||||
def string_to_num(string):
|
def string_to_num(string):
|
||||||
@@ -419,7 +420,7 @@ class Device(HasLscIO, Module):
|
|||||||
"""check whether a returned calibration point is equal within curve point precision"""
|
"""check whether a returned calibration point is equal within curve point precision"""
|
||||||
for v1, v2, eps in zip(left, right, fixeps):
|
for v1, v2, eps in zip(left, right, fixeps):
|
||||||
try:
|
try:
|
||||||
assert_approx_equal(v1, v2, significant, verbose=False)
|
np_testing.assert_approx_equal(v1, v2, significant, verbose=False)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
return abs(v1 - v2) < eps
|
return abs(v1 - v2) < eps
|
||||||
return True
|
return True
|
||||||
@@ -464,7 +465,7 @@ class CurveRequest:
|
|||||||
self.action = device.find_curve
|
self.action = device.find_curve
|
||||||
self.new_sensors = set()
|
self.new_sensors = set()
|
||||||
self.sensors = {sensor.channel: sensor}
|
self.sensors = {sensor.channel: sensor}
|
||||||
calcurve = CalCurve(sensor.calcurve)
|
calcurve = calcurve_module.CalCurve(sensor.calcurve)
|
||||||
equipment_id = device.propertyValues.get('original_id') or device.secNode.equipment_id
|
equipment_id = device.propertyValues.get('original_id') or device.secNode.equipment_id
|
||||||
name = f"{equipment_id.split('.')[0]}.{sensor.name}"
|
name = f"{equipment_id.split('.')[0]}.{sensor.name}"
|
||||||
sn = calcurve.calibname
|
sn = calcurve.calibname
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ import sys
|
|||||||
from time import monotonic
|
from time import monotonic
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
import snap7
|
import snap7
|
||||||
from frappy.core import Attached, Command, Readable, Parameter, FloatRange, HasIO, Property, \
|
from frappy.core import Attached, Command, Readable, Parameter, FloatRange, HasIO, Property, StringType, \
|
||||||
IDLE, BUSY, WARN, ERROR, Writable, Drivable, Communicator
|
IDLE, BUSY, WARN, ERROR, Writable, Drivable, BoolType, IntRange, Communicator, StatusType
|
||||||
from frappy.datatypes import StringType, BoolType, IntRange, NoneOr, Int32
|
|
||||||
from frappy.errors import CommunicationFailedError, ConfigError
|
from frappy.errors import CommunicationFailedError, ConfigError
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
@@ -81,16 +80,11 @@ class IO(Communicator):
|
|||||||
class LogoMixin(HasIO):
|
class LogoMixin(HasIO):
|
||||||
ioclass = IO
|
ioclass = IO
|
||||||
|
|
||||||
def get_vm_value(self, addr, scale=None):
|
def get_vm_value(self, vm_address):
|
||||||
if scale is None:
|
return literal_eval(self.io.communicate(vm_address))
|
||||||
return int(self.io.communicate(addr))
|
|
||||||
return float(self.io.communicate(addr)) * scale
|
|
||||||
|
|
||||||
def set_vm_value(self, addr, value, scale=None):
|
def set_vm_value(self, vm_address, value):
|
||||||
if scale is None:
|
return literal_eval(self.io.communicate(f'{vm_address} {round(value)}'))
|
||||||
return int(self.io.communicate(f'{addr} {value}'))
|
|
||||||
reply = self.io.communicate(f'{addr} {round(value / scale)}')
|
|
||||||
return int(reply) * scale
|
|
||||||
|
|
||||||
|
|
||||||
class DigitalActuator(LogoMixin, Writable):
|
class DigitalActuator(LogoMixin, Writable):
|
||||||
@@ -225,47 +219,195 @@ class DelayedActuator(DigitalActuator, Drivable):
|
|||||||
self._pulse_end = now + delay
|
self._pulse_end = now + delay
|
||||||
|
|
||||||
|
|
||||||
class Sensor(LogoMixin, Readable):
|
class Value(LogoMixin, Readable):
|
||||||
addr = Property('VM address', datatype=StringType())
|
addr = Property('VM address', datatype=StringType())
|
||||||
scale = Property('scale to multiply with raw integer value',
|
|
||||||
NoneOr(FloatRange()), default=None)
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.get_vm_value(self.addr, self.scale)
|
return self.get_vm_value(self.addr)
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
return IDLE, ''
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
class AnalogOutput(Sensor, Writable):
|
class DigitalValue(Value):
|
||||||
output_addr = Property('VM address output', datatype=StringType(), default='')
|
value = Parameter('airpressure state', datatype=BoolType())
|
||||||
|
|
||||||
def checkProperties(self):
|
|
||||||
super().checkProperties()
|
|
||||||
if not self.output_addr:
|
|
||||||
self.output_addr = self.addr
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
return self.get_vm_value(self.addr, self.scale)
|
|
||||||
|
|
||||||
def write_target(self, target):
|
|
||||||
return self.set_vm_value(self.output_addr, target, self.scale)
|
|
||||||
|
|
||||||
|
|
||||||
class Pressure(Sensor):
|
# TODO: the following classes are too specific, they have to be moved
|
||||||
|
|
||||||
|
class Pressure(LogoMixin, Drivable):
|
||||||
|
vm_address = Property('VM address', datatype=StringType())
|
||||||
value = Parameter('pressure', datatype=FloatRange(unit='mbar'))
|
value = Parameter('pressure', datatype=FloatRange(unit='mbar'))
|
||||||
|
|
||||||
|
# pollinterval = 0.5
|
||||||
class Resistor(Sensor):
|
|
||||||
value = Parameter('resistance', datatype=FloatRange(unit='Ohm'))
|
|
||||||
|
|
||||||
|
|
||||||
class Comparator(LogoMixin, Readable):
|
|
||||||
addr = Property('VM address', datatype=StringType())
|
|
||||||
scale = Property('scale to multiply with raw integer value',
|
|
||||||
NoneOr(FloatRange()), default=None)
|
|
||||||
value = Parameter('airpressure state', datatype=BoolType())
|
|
||||||
threshold = Property('threshold for True', FloatRange())
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.get_vm_value(self.addr, self.scale) > self.threshold
|
return self.get_vm_value(self.vm_address)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class Airpressure(LogoMixin, Readable):
|
||||||
|
vm_address = Property('VM address', datatype=StringType())
|
||||||
|
value = Parameter('airpressure state', datatype=BoolType())
|
||||||
|
|
||||||
|
# pollinterval = 0.5
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if (self.get_vm_value(self.vm_address) > 500):
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class Valve(LogoMixin, Drivable):
|
||||||
|
vm_address_input = Property('VM address input', datatype=StringType())
|
||||||
|
vm_address_output = Property('VM address output', datatype=StringType())
|
||||||
|
|
||||||
|
target = Parameter('Valve target', datatype=BoolType())
|
||||||
|
value = Parameter('Value state', datatype=BoolType())
|
||||||
|
_remaining_tries = None
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address_input)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.set_vm_value(self.vm_address_output, target)
|
||||||
|
self._remaining_tries = 5
|
||||||
|
self.status = BUSY, 'switching'
|
||||||
|
self.setFastPoll(True, 0.5)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
self.log.debug('read_status')
|
||||||
|
value = self.read_value()
|
||||||
|
self.log.debug('value %d target %d', value, self.target)
|
||||||
|
if value != self.target:
|
||||||
|
if self._remaining_tries is None:
|
||||||
|
self.target = self.read_value()
|
||||||
|
return IDLE, ''
|
||||||
|
self._remaining_tries -= 1
|
||||||
|
if self._remaining_tries < 0:
|
||||||
|
self.setFastPoll(False)
|
||||||
|
return ERROR, 'too many tries to switch'
|
||||||
|
self.set_vm_value(self.vm_address_output, self.target)
|
||||||
|
return BUSY, 'switching (try again)'
|
||||||
|
self.setFastPoll(False)
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class FluidMachines(LogoMixin, Drivable):
|
||||||
|
vm_address_output = Property('VM address output', datatype=StringType())
|
||||||
|
|
||||||
|
target = Parameter('Valve target', datatype=BoolType())
|
||||||
|
value = Parameter('Valve state', datatype=BoolType())
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address_output)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
return self.set_vm_value(self.vm_address_output, target)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class TempSensor(LogoMixin, Readable):
|
||||||
|
vm_address = Property('VM address', datatype=StringType())
|
||||||
|
value = Parameter('resistance', datatype=FloatRange(unit='Ohm'))
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class HeaterParam(LogoMixin, Writable):
|
||||||
|
vm_address = Property('VM address output', datatype=StringType())
|
||||||
|
|
||||||
|
target = Parameter('Heater target', datatype=IntRange())
|
||||||
|
|
||||||
|
value = Parameter('Heater Param', datatype=IntRange())
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
return self.set_vm_value(self.vm_address, target)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class controlHeater(LogoMixin, Writable):
|
||||||
|
vm_address = Property('VM address on switch', datatype=StringType())
|
||||||
|
|
||||||
|
target = Parameter('Heater state', datatype=BoolType())
|
||||||
|
|
||||||
|
value = Parameter('Heater state', datatype=BoolType())
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address_on)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if (target):
|
||||||
|
return self.set_vm_value(self.vm_address, True)
|
||||||
|
else:
|
||||||
|
return self.set_vm_value(self.vm_address, False)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class safetyfeatureState(LogoMixin, Readable):
|
||||||
|
vm_address = Property('VM address state', datatype=StringType())
|
||||||
|
|
||||||
|
value = Parameter('safety Feature state', datatype=BoolType())
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class safetyfeatureParam(LogoMixin, Writable):
|
||||||
|
vm_address = Property('VM address output', datatype=StringType())
|
||||||
|
|
||||||
|
target = Parameter('safety Feature target', datatype=IntRange())
|
||||||
|
|
||||||
|
value = Parameter('safety Feature Param', datatype=IntRange())
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
return self.set_vm_value(self.vm_address, target)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class comparatorgekoppeltParam(LogoMixin, Writable):
|
||||||
|
vm_address_1 = Property('VM address output', datatype=StringType())
|
||||||
|
vm_address_2 = Property('VM address output', datatype=StringType())
|
||||||
|
|
||||||
|
target = Parameter('safety Feature target', datatype=IntRange())
|
||||||
|
value = Parameter('safety Feature Param', datatype=IntRange())
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.get_vm_value(self.vm_address_1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.set_vm_value(self.vm_address_1, target)
|
||||||
|
return self.set_vm_value(self.vm_address_2, target)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ def parse_result(reply):
|
|||||||
|
|
||||||
|
|
||||||
class LakeShoreIO(HasIO):
|
class LakeShoreIO(HasIO):
|
||||||
|
ioClass = StringIO
|
||||||
|
|
||||||
def set_param(self, cmd, *args):
|
def set_param(self, cmd, *args):
|
||||||
args = [f'{a:g}' for a in args]
|
args = [f'{a:g}' for a in args]
|
||||||
if ' ' in cmd.strip():
|
if ' ' in cmd.strip():
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class SimpleMagfield(HasStates, Drivable):
|
|||||||
'trained field (positive)',
|
'trained field (positive)',
|
||||||
TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')),
|
TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')),
|
||||||
readonly=False, default=(0, 0))
|
readonly=False, default=(0, 0))
|
||||||
|
trainmode = Parameter('train mode flag', EnumType(off=0, on=1, undef=2), default=2)
|
||||||
wait_stable_field = Parameter(
|
wait_stable_field = Parameter(
|
||||||
'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31)
|
'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31)
|
||||||
ramp_tmo = Parameter(
|
ramp_tmo = Parameter(
|
||||||
@@ -149,10 +150,24 @@ class SimpleMagfield(HasStates, Drivable):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def handle_train_mode(self):
|
||||||
|
self.log.info('handle %r %r', self.trained, self.value)
|
||||||
|
if self.trained[0] < self.value < self.trained[1]:
|
||||||
|
trainmode = 'off'
|
||||||
|
else:
|
||||||
|
trainmode = 'on'
|
||||||
|
if self.value > 0:
|
||||||
|
self.trained = (self.trained[0], max(self.trained[1], self.value))
|
||||||
|
else:
|
||||||
|
self.trained = (min(self.trained[0], self.value), self.trained[1])
|
||||||
|
if self.trainmode != trainmode:
|
||||||
|
self.write_trainmode(trainmode)
|
||||||
|
|
||||||
@status_code(BUSY, 'ramping field')
|
@status_code(BUSY, 'ramping field')
|
||||||
def ramp_to_target(self, sm):
|
def ramp_to_target(self, sm):
|
||||||
if sm.init:
|
if sm.init:
|
||||||
self.init_progress(sm, self.value)
|
self.init_progress(sm, self.value)
|
||||||
|
self.handle_train_mode()
|
||||||
# Remarks: assume there is a ramp limiting feature
|
# Remarks: assume there is a ramp limiting feature
|
||||||
if abs(self.value - sm.target) > self.tolerance:
|
if abs(self.value - sm.target) > self.tolerance:
|
||||||
if self.get_progress(sm, self.value) > self.ramp_tmo:
|
if self.get_progress(sm, self.value) > self.ramp_tmo:
|
||||||
@@ -166,11 +181,15 @@ class SimpleMagfield(HasStates, Drivable):
|
|||||||
def stabilize_field(self, sm):
|
def stabilize_field(self, sm):
|
||||||
if sm.now - sm.stabilize_start < self.wait_stable_field:
|
if sm.now - sm.stabilize_start < self.wait_stable_field:
|
||||||
return Retry
|
return Retry
|
||||||
|
self.handle_train_mode()
|
||||||
return self.final_status()
|
return self.final_status()
|
||||||
|
|
||||||
def read_workingramp(self):
|
def read_workingramp(self):
|
||||||
return self.ramp
|
return self.ramp
|
||||||
|
|
||||||
|
def write_trainmode(self, value):
|
||||||
|
"""overwrite when needed"""
|
||||||
|
|
||||||
|
|
||||||
class Magfield(SimpleMagfield):
|
class Magfield(SimpleMagfield):
|
||||||
status = Parameter(datatype=StatusType(Status))
|
status = Parameter(datatype=StatusType(Status))
|
||||||
@@ -335,6 +354,7 @@ class Magfield(SimpleMagfield):
|
|||||||
|
|
||||||
@status_code(Status.RAMPING)
|
@status_code(Status.RAMPING)
|
||||||
def ramp_to_target(self, sm):
|
def ramp_to_target(self, sm):
|
||||||
|
self.handle_train_mode()
|
||||||
dif = abs(self.value - sm.target)
|
dif = abs(self.value - sm.target)
|
||||||
if sm.init:
|
if sm.init:
|
||||||
sm.stabilize_start = 0 # in case current is already at target
|
sm.stabilize_start = 0 # in case current is already at target
|
||||||
@@ -353,6 +373,7 @@ class Magfield(SimpleMagfield):
|
|||||||
|
|
||||||
@status_code(Status.STABILIZING)
|
@status_code(Status.STABILIZING)
|
||||||
def stabilize_field(self, sm):
|
def stabilize_field(self, sm):
|
||||||
|
self.handle_train_mode()
|
||||||
if sm.now < sm.stabilize_start + self.wait_stable_field:
|
if sm.now < sm.stabilize_start + self.wait_stable_field:
|
||||||
return Retry
|
return Retry
|
||||||
return self.check_switch_off
|
return self.check_switch_off
|
||||||
|
|||||||
@@ -64,9 +64,15 @@ fast_slow = Mapped(ON=0, OFF=1) # maps OIs slow=ON/fast=OFF to sample_rate.slow
|
|||||||
class IO(StringIO):
|
class IO(StringIO):
|
||||||
identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')]
|
identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')]
|
||||||
timeout = 5
|
timeout = 5
|
||||||
|
encoding = 'latin1'
|
||||||
|
|
||||||
|
@Command(StringType(), result=StringType(isUTF8=True))
|
||||||
|
def communicate(self, cmd, noreply=False):
|
||||||
|
return super().communicate(cmd, noreply)
|
||||||
|
|
||||||
|
|
||||||
class MercuryChannel(HasIO):
|
class MercuryChannel(HasIO):
|
||||||
|
ioClass = IO
|
||||||
slot = Property('comma separated slot id(s), e.g. DB6.T1', StringType())
|
slot = Property('comma separated slot id(s), e.g. DB6.T1', StringType())
|
||||||
kind = '' #: used slot kind(s)
|
kind = '' #: used slot kind(s)
|
||||||
slots = () #: dict[<kind>] of <slot>
|
slots = () #: dict[<kind>] of <slot>
|
||||||
|
|||||||
715
frappy_psi/oiclassic.py
Normal file
715
frappy_psi/oiclassic.py
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# *****************************************************************************
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
# Anik Stark <anik.stark@psi.ch>
|
||||||
|
# *****************************************************************************
|
||||||
|
"""oxford instruments old (classic) devices (ILM, IGH, IPS)"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from frappy.core import Parameter, Property, EnumType, FloatRange, BoolType, \
|
||||||
|
StringIO, HasIO, Readable, Writable, Drivable, IDLE, BUSY, WARN, ERROR, Attached
|
||||||
|
from frappy.lib import formatStatusBits
|
||||||
|
from frappy.lib.enum import Enum
|
||||||
|
from frappy.errors import BadValueError, HardwareError, CommunicationFailedError
|
||||||
|
from frappy_psi.magfield import Magfield, Status
|
||||||
|
from frappy.states import Retry
|
||||||
|
|
||||||
|
|
||||||
|
def bit(x, pos):
|
||||||
|
"""Check if the bit at a certain position is set"""
|
||||||
|
return bool(x & (1 << pos))
|
||||||
|
|
||||||
|
|
||||||
|
class OxBase(HasIO):
|
||||||
|
|
||||||
|
def query(self, cmd, scale=None):
|
||||||
|
reply = self.communicate(cmd)
|
||||||
|
if reply[0] != cmd[0]:
|
||||||
|
raise CommunicationFailedError(f'bad reply: {reply} to command {cmd}')
|
||||||
|
if scale is None:
|
||||||
|
return int(reply[1:])
|
||||||
|
return float(reply[1:]) * scale
|
||||||
|
|
||||||
|
def change(self, cmd, value, scale=None):
|
||||||
|
try:
|
||||||
|
self.communicate('C3')
|
||||||
|
reply = self.communicate(f'{cmd}{round(value / scale)}')
|
||||||
|
if reply[0] != cmd[0]:
|
||||||
|
raise CommunicationFailedError(f'bad reply: {reply}')
|
||||||
|
finally:
|
||||||
|
self.communicate('C0')
|
||||||
|
|
||||||
|
def command(self, *cmds):
|
||||||
|
try:
|
||||||
|
self.communicate('C3')
|
||||||
|
for cmd in cmds:
|
||||||
|
self.communicate(cmd)
|
||||||
|
finally:
|
||||||
|
self.communicate('C0')
|
||||||
|
|
||||||
|
|
||||||
|
class IPS_IO(StringIO):
|
||||||
|
"""oxford instruments power supply IPS120-10"""
|
||||||
|
end_of_line = '\r'
|
||||||
|
identification = [('V', r'IPS120-10.*')] # instrument type and software version
|
||||||
|
default_settings = {'baudrate': 9600}
|
||||||
|
|
||||||
|
|
||||||
|
Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=4)
|
||||||
|
status_map = {'0': (IDLE, ''),
|
||||||
|
'1': (ERROR, 'quenched'),
|
||||||
|
'2': (ERROR, 'overheated'),
|
||||||
|
'4': (WARN, 'warming up'),
|
||||||
|
'8': (ERROR, '')
|
||||||
|
}
|
||||||
|
limit_map = {'0': (IDLE, ''),
|
||||||
|
'1': (WARN, 'on positive voltage limit'),
|
||||||
|
'2': (WARN, 'on negative voltage limit'),
|
||||||
|
'4': (ERROR, 'outside negative current limit'),
|
||||||
|
'8': (ERROR, 'outside positive current limit')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Field(OxBase, Magfield):
|
||||||
|
""" read commands:
|
||||||
|
R1 measured power supply voltage (V)
|
||||||
|
R7 demand field (output field) (T)
|
||||||
|
R8 setpoint (target field) (T)
|
||||||
|
R9 sweep field rate (T/min)
|
||||||
|
R18 persistent field (T)
|
||||||
|
X Status
|
||||||
|
|
||||||
|
control commands:
|
||||||
|
A set activity
|
||||||
|
T set field sweep rate
|
||||||
|
H set switch heater
|
||||||
|
J set target field """
|
||||||
|
|
||||||
|
ioClass = IPS_IO
|
||||||
|
|
||||||
|
action = Parameter('action', EnumType(Action), readonly=False)
|
||||||
|
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
|
||||||
|
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
|
||||||
|
persistent_field = Parameter(
|
||||||
|
'persistent field at last switch off', FloatRange(unit='T'), readonly=False)
|
||||||
|
wait_switch_on = Parameter(default=15)
|
||||||
|
wait_switch_off = Parameter(default=15)
|
||||||
|
wait_stable_field = Parameter(default=10)
|
||||||
|
forced_persistent_field = Parameter(
|
||||||
|
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
|
||||||
|
switch_heater = Parameter('turn switch heater on/off', EnumType(off=0, on=1, forced=2), default=0)
|
||||||
|
|
||||||
|
_field_mismatch = None
|
||||||
|
__persistent_field = None # internal value of persistent field
|
||||||
|
_status = '00'
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
try:
|
||||||
|
self.write_action(Action.hold)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error('can not set to hold %r', e)
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
super().doPoll()
|
||||||
|
self.read_current()
|
||||||
|
|
||||||
|
def initialReads(self):
|
||||||
|
# on restart, assume switch is changed long time ago, if not, the mercury
|
||||||
|
# will complain and this will be handled in start_ramp_to_field
|
||||||
|
self.switch_on_time = 0
|
||||||
|
self.switch_off_time = 0
|
||||||
|
super().initialReads()
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self.switch_heater:
|
||||||
|
self.__persistent_field = self.query('R7')
|
||||||
|
self.forced_persistent_field = False
|
||||||
|
self._field_mismatch = False
|
||||||
|
return self.__persistent_field
|
||||||
|
pf = self.query('R18')
|
||||||
|
if self.__persistent_field is None:
|
||||||
|
self.__persistent_field = pf
|
||||||
|
self._field_mismatch = False
|
||||||
|
else:
|
||||||
|
self._field_mismatch = abs(self.__persistent_field - pf) > self.tolerance * 10
|
||||||
|
self.persistent_field = self.__persistent_field
|
||||||
|
return self.__persistent_field
|
||||||
|
|
||||||
|
def read_ramp(self):
|
||||||
|
return self.query('R9')
|
||||||
|
|
||||||
|
def write_ramp(self, value):
|
||||||
|
self.change('T', value)
|
||||||
|
return self.read_ramp()
|
||||||
|
|
||||||
|
def write_action(self, value):
|
||||||
|
self.change('A', int(value))
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
def read_voltage(self):
|
||||||
|
return self.query('R1')
|
||||||
|
|
||||||
|
def read_setpoint(self):
|
||||||
|
return self.query('R8')
|
||||||
|
|
||||||
|
def read_current(self):
|
||||||
|
return self.query('R7')
|
||||||
|
|
||||||
|
def write_persistent_field(self, value):
|
||||||
|
if self.forced_persistent_field or abs(self.__persistent_field - value) <= self.tolerance * 10:
|
||||||
|
self._field_mismatch = False
|
||||||
|
self.__persistent_field = value
|
||||||
|
return value
|
||||||
|
raise BadValueError('changing persistent field needs forced_persistent_field=True')
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if self._field_mismatch:
|
||||||
|
self.forced_persistent_field = True
|
||||||
|
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
|
||||||
|
return super().write_target(target)
|
||||||
|
|
||||||
|
def read_switch_heater(self):
|
||||||
|
self.read_status()
|
||||||
|
return self.switch_heater
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
status = self.communicate('X')
|
||||||
|
match = re.match(r'X(\d\d)A(\d)C\dH(\d)M\d\dP\d\d', status)
|
||||||
|
if match is None:
|
||||||
|
raise CommunicationFailedError(f'unexpected status: {status}')
|
||||||
|
self._status = match.group(1)
|
||||||
|
self.action = int(match.group(2))
|
||||||
|
self.switch_heater = match.group(3) == '1'
|
||||||
|
if self._status[0] != '0':
|
||||||
|
self._state_machine.stop()
|
||||||
|
return status_map.get(self._status[0], (ERROR, f'bad status: {self._status}'))
|
||||||
|
if self._status[1] != '0':
|
||||||
|
return limit_map.get(self._status[1], (ERROR, f'bad status: {self._status}')) # need to stop sm too?
|
||||||
|
return super().read_status()
|
||||||
|
|
||||||
|
def write_switch_heater(self, value):
|
||||||
|
if value == self.read_switch_heater():
|
||||||
|
self.log.info('switch heater already %r', value)
|
||||||
|
# we do not want to restart the timer
|
||||||
|
return value
|
||||||
|
self.log.debug('switch time fixed for 10 sec')
|
||||||
|
self.change('H', int(value))
|
||||||
|
#return result
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
def set_and_go(self, value):
|
||||||
|
self.change('J', value)
|
||||||
|
self.setpoint = self.read_current()
|
||||||
|
assert self.write_action(Action.hold) == Action.hold
|
||||||
|
assert self.write_action(Action.run_to_set) == Action.run_to_set
|
||||||
|
|
||||||
|
def ramp_to_target(self, sm):
|
||||||
|
try:
|
||||||
|
return super().ramp_to_target(sm)
|
||||||
|
except HardwareError:
|
||||||
|
sm.try_cnt -= 1
|
||||||
|
if sm.try_cnt < 0:
|
||||||
|
raise
|
||||||
|
self.set_and_go(sm.target)
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
def final_status(self, *args, **kwds):
|
||||||
|
self.write_action(Action.hold)
|
||||||
|
return super().final_status(*args, **kwds)
|
||||||
|
|
||||||
|
def on_restart(self, sm):
|
||||||
|
self.write_action(Action.hold)
|
||||||
|
return super().on_restart(sm)
|
||||||
|
|
||||||
|
def start_ramp_to_field(self, sm):
|
||||||
|
if abs(self.current - self.__persistent_field) <= self.tolerance:
|
||||||
|
self.log.info('leads %g are already at %g', self.current, self.__persistent_field)
|
||||||
|
return self.ramp_to_field
|
||||||
|
try:
|
||||||
|
self.set_and_go(self.__persistent_field)
|
||||||
|
except (HardwareError, AssertionError) as e:
|
||||||
|
if self.switch_heater:
|
||||||
|
self.log.warn('switch is already on!')
|
||||||
|
return self.ramp_to_field
|
||||||
|
self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.__persistent_field, e)
|
||||||
|
sm.after_wait = self.ramp_to_field
|
||||||
|
return self.wait_for_switch
|
||||||
|
return self.ramp_to_field
|
||||||
|
|
||||||
|
def start_ramp_to_target(self, sm):
|
||||||
|
sm.try_cnt = 5
|
||||||
|
try:
|
||||||
|
self.set_and_go(sm.target)
|
||||||
|
except (HardwareError, AssertionError) as e:
|
||||||
|
self.log.warn('switch not yet ready %r', e)
|
||||||
|
self.status = Status.PREPARING, 'wait for switch on'
|
||||||
|
sm.after_wait = self.ramp_to_target
|
||||||
|
return self.wait_for_switch
|
||||||
|
return self.ramp_to_target
|
||||||
|
|
||||||
|
def ramp_to_field(self, sm):
|
||||||
|
try:
|
||||||
|
return super().ramp_to_field(sm)
|
||||||
|
except HardwareError:
|
||||||
|
sm.try_cnt -= 1
|
||||||
|
if sm.try_cnt < 0:
|
||||||
|
raise
|
||||||
|
self.set_and_go(self.__persistent_field)
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
def wait_for_switch(self, sm):
|
||||||
|
if not sm.delta(10):
|
||||||
|
return Retry
|
||||||
|
try:
|
||||||
|
self.log.warn('try again')
|
||||||
|
# try again
|
||||||
|
self.set_and_go(self.__persistent_field)
|
||||||
|
except (HardwareError, AssertionError):
|
||||||
|
return Retry
|
||||||
|
return sm.after_wait
|
||||||
|
|
||||||
|
def wait_for_switch_on(self, sm):
|
||||||
|
self.read_switch_heater() # trigger switch_on/off_time
|
||||||
|
if self.switch_heater == self.switch_heater.off:
|
||||||
|
if sm.init: # avoid too many states chained
|
||||||
|
return Retry
|
||||||
|
self.log.warning('switch turned off manually?')
|
||||||
|
return self.start_switch_on
|
||||||
|
return super().wait_for_switch_on(sm)
|
||||||
|
|
||||||
|
def wait_for_switch_off(self, sm):
|
||||||
|
self.read_switch_heater()
|
||||||
|
if self.switch_heater == self.switch_heater.on:
|
||||||
|
if sm.init: # avoid too many states chained
|
||||||
|
return Retry
|
||||||
|
self.log.warning('switch turned on manually?')
|
||||||
|
return self.start_switch_off
|
||||||
|
return super().wait_for_switch_off(sm)
|
||||||
|
|
||||||
|
def start_ramp_to_zero(self, sm):
|
||||||
|
pf = self.query('R18')
|
||||||
|
if abs(pf - self.value) > self.tolerance * 10:
|
||||||
|
self.log.warning('persistent field %g does not match %g after switch off', pf, self.value)
|
||||||
|
try:
|
||||||
|
assert self.write_action(Action.hold) == Action.hold
|
||||||
|
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
||||||
|
except (HardwareError, AssertionError) as e:
|
||||||
|
self.log.warn('switch not yet ready %r', e)
|
||||||
|
self.status = Status.PREPARING, 'wait for switch off'
|
||||||
|
sm.after_wait = self.ramp_to_zero
|
||||||
|
return self.wait_for_switch
|
||||||
|
return self.ramp_to_zero
|
||||||
|
|
||||||
|
def ramp_to_zero(self, sm):
|
||||||
|
try:
|
||||||
|
return super().ramp_to_zero(sm)
|
||||||
|
except HardwareError:
|
||||||
|
sm.try_cnt -= 1
|
||||||
|
if sm.try_cnt < 0:
|
||||||
|
raise
|
||||||
|
assert self.write_action(Action.hold) == Action.hold
|
||||||
|
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
def write_trainmode(self, value):
|
||||||
|
self.change('M', '5' if value == 'off' else '1')
|
||||||
|
|
||||||
|
|
||||||
|
class ILM_IO(StringIO):
|
||||||
|
"""oxford instruments level meter ILM200"""
|
||||||
|
end_of_line = '\r'
|
||||||
|
identification = [('V', r'ILM200.*')] # instrument type and software version
|
||||||
|
default_settings = {'baudrate': 9600}
|
||||||
|
timeout = 5
|
||||||
|
|
||||||
|
|
||||||
|
class Level(OxBase, Readable):
|
||||||
|
|
||||||
|
""" X code: XcccSuuvvwwRzz
|
||||||
|
c: position corresponds to channel 1, 2, 3
|
||||||
|
possible values in each position are 0, 1, 2, 3, 9
|
||||||
|
vv, uu, ww: channel status for channel 1, 2, 3 respectively, 2 bits each
|
||||||
|
zz: relay status """
|
||||||
|
|
||||||
|
ioClass = ILM_IO
|
||||||
|
|
||||||
|
value = Parameter('level', datatype=FloatRange(unit='%'))
|
||||||
|
fast = Parameter('fast reading', datatype=BoolType())
|
||||||
|
CHANNEL = None
|
||||||
|
X_PATTERN = re.compile(r'X(\d)(\d)(\d)S([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})R\d\d$')
|
||||||
|
MEDIUM = None
|
||||||
|
_statusbits = None
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.query(f'R{self.CHANNEL}', 0.1)
|
||||||
|
|
||||||
|
def write_fast(self, fast):
|
||||||
|
self.command(f'T{self.CHANNEL}' if fast else f'S{self.CHANNEL}')
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
reply = self.communicate('X')
|
||||||
|
match = self.X_PATTERN.match(reply)
|
||||||
|
if match:
|
||||||
|
statuslist = match.groups()
|
||||||
|
if statuslist[self.CHANNEL] == '9':
|
||||||
|
return ERROR, f'error on {self.MEDIUM} level channel (not connected?)'
|
||||||
|
if (statuslist[self.CHANNEL] == '1') != (self.MEDIUM == 'N2'):
|
||||||
|
# '1': channel is used for N2
|
||||||
|
return ERROR, f'{self.MEDIUM} level channel not configured properly'
|
||||||
|
self._statusbits = int(statuslist[self.CHANNEL + 3], 16)
|
||||||
|
return None
|
||||||
|
return ERROR, f'bad status message {reply}'
|
||||||
|
|
||||||
|
|
||||||
|
class HeLevel(Level):
|
||||||
|
|
||||||
|
value = Parameter('He level', FloatRange(unit='%'))
|
||||||
|
fast = Parameter('switching fast/slow', datatype=BoolType(), readonly=False)
|
||||||
|
CHANNEL = 1
|
||||||
|
MEDIUM = 'He'
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
status = self.get_status()
|
||||||
|
if status is not None:
|
||||||
|
return status
|
||||||
|
return IDLE, formatStatusBits(self._statusbits, ['meas', 'fast', 'slow'])
|
||||||
|
|
||||||
|
|
||||||
|
class N2Level(Level):
|
||||||
|
|
||||||
|
ioClass = ILM_IO
|
||||||
|
|
||||||
|
value = Parameter('N2 level', FloatRange(unit='%'))
|
||||||
|
CHANNEL = 2
|
||||||
|
MEDIUM = 'N2'
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
status = self.get_status()
|
||||||
|
if status is not None:
|
||||||
|
return status
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
VALVE_MAP = {'V9': 1,
|
||||||
|
'V8': 2,
|
||||||
|
'V7': 3,
|
||||||
|
'V11A': 4,
|
||||||
|
'V13A': 5,
|
||||||
|
'V13B': 6,
|
||||||
|
'V11B': 7,
|
||||||
|
'V12B': 8,
|
||||||
|
'rotary_pump_He4': 9,
|
||||||
|
'V1': 10,
|
||||||
|
'V5': 11,
|
||||||
|
'V4': 12,
|
||||||
|
'V3': 13,
|
||||||
|
'V14' : 14,
|
||||||
|
'V10': 15,
|
||||||
|
'V2': 16,
|
||||||
|
'V2A_He4': 17,
|
||||||
|
'V1A_He4': 18,
|
||||||
|
'V5A_He4': 19,
|
||||||
|
'V4A_He4': 20,
|
||||||
|
'V3A_He4': 21,
|
||||||
|
'roots_pump': 22,
|
||||||
|
'unlabeled_pump': 23,
|
||||||
|
'rotary_pump_He3': 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IGH_IO(StringIO):
|
||||||
|
""" oxford instruments dilution gas handling Kelvinox IGH
|
||||||
|
|
||||||
|
X code: XxAaCcPpppSsOoEe
|
||||||
|
x motorized valves are still initializing
|
||||||
|
a mix heater activity
|
||||||
|
c control status (0, 1, 2, 3)
|
||||||
|
pppp 4 hex numbers (two digits each), state of solenoid valves and pumps
|
||||||
|
s hex digit, state of the 3 motorized valves
|
||||||
|
o still and sorb heater information
|
||||||
|
e mix heater power range """
|
||||||
|
|
||||||
|
end_of_line = '\r'
|
||||||
|
identification = [('V', r'IGH.*')]
|
||||||
|
default_settings = {'baudrate': 9600}
|
||||||
|
|
||||||
|
X_PATTERN = re.compile(r'X(\d)A(\d)C\dP([0-9A-F]{8})S([0-9A-F])O(\d)E(\d)$')
|
||||||
|
_ini_valves = 0 # ini status of motorized valves
|
||||||
|
_mix_status = 0
|
||||||
|
_valves = 0 # status of solenoid valves and pumps
|
||||||
|
_motor_status = 0
|
||||||
|
_heater_status = 0
|
||||||
|
_heater_range = 0
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
reply = self.communicate('X')
|
||||||
|
match = self.X_PATTERN.match(reply)
|
||||||
|
if match:
|
||||||
|
ini_valves, mix_status, valves, motor_status, heater_status, heater_range = match.groups()
|
||||||
|
self._ini_valves = int(ini_valves, 16)
|
||||||
|
self._mix_status = int(mix_status)
|
||||||
|
self._valves = int(valves, 16)
|
||||||
|
self._motor_status = int(motor_status, 16)
|
||||||
|
self._heater_status = int(heater_status)
|
||||||
|
self._heater_range = int(heater_range)
|
||||||
|
|
||||||
|
|
||||||
|
class Valve(OxBase, Writable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
value = Parameter('state of valve (open or close)', datatype=EnumType(open=1, close=0))
|
||||||
|
target = Parameter('open or close valve', datatype=EnumType(open=1, close=0))
|
||||||
|
addr = Property('valve name', datatype=EnumType(VALVE_MAP))
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
# hex -> int -> check if bit in bin(integer) is set at the addr position
|
||||||
|
return bit(self.io._valves, self.addr.value - 1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
# open: 2N, close: 2N + 1
|
||||||
|
self.change('P', (2 * self.addr.value + 1 - int(target)), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class PulsedValve(Valve):
|
||||||
|
|
||||||
|
delay = Parameter('delay (time valve is open)', FloatRange(unit='s'), readonly=False)
|
||||||
|
_start = 0
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if target:
|
||||||
|
self._start = time.time()
|
||||||
|
self.setFastPoll(True, 0.01)
|
||||||
|
else:
|
||||||
|
self.setFastPoll(False)
|
||||||
|
self.change('P', (2 * self.addr.value + 1 - int(target)), 1)
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
super().doPoll()
|
||||||
|
if self._start:
|
||||||
|
if time.time() > self._start + self.delay:
|
||||||
|
self.write_target(0)
|
||||||
|
self._start = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MotorValve(OxBase, Writable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('target of motor valve', datatype=FloatRange(0, 100, unit='%'))
|
||||||
|
value = Parameter('position of fast valve', datatype=FloatRange(0, 100, unit='%'))
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.change('H', target, 0.1) # valve V12A
|
||||||
|
self.value = target
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.target
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if bit(self.io._ini_valves, 1):
|
||||||
|
self.value = 0
|
||||||
|
return BUSY, 'valve V12A is initializing'
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
|
||||||
|
class SlowMotorValve(OxBase, Drivable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('target of slow motor valve', datatype=FloatRange(0, 100, unit='%', fmtstr='%.1f'))
|
||||||
|
value = Parameter('position of slow valve', datatype=FloatRange(0, 100, unit='%', fmtstr='%.1f'))
|
||||||
|
_prev_time = 0
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
return self.query('R7', 0.1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.change('G', target, 0.1) # valve V6
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if bit(self.io._ini_valves, 0):
|
||||||
|
self.value = 0
|
||||||
|
return BUSY, 'valve V6 is initializing'
|
||||||
|
now = time.time()
|
||||||
|
if self._prev_time == 0:
|
||||||
|
self.value = self.read_target()
|
||||||
|
delta_t = 0
|
||||||
|
else:
|
||||||
|
delta_t = now - self._prev_time
|
||||||
|
self._prev_time = now
|
||||||
|
if (self.io._motor_status >> 0) & 1:
|
||||||
|
if self.target > self.value:
|
||||||
|
self.value = min(self.target, self.value + delta_t / 300 * 100)
|
||||||
|
else:
|
||||||
|
self.value = max(self.target, self.value - delta_t / 300 * 100)
|
||||||
|
return BUSY, 'valve V6 is moving'
|
||||||
|
self.value = self.target
|
||||||
|
return IDLE, ''
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""stop moving"""
|
||||||
|
self.write_target(self.value)
|
||||||
|
|
||||||
|
|
||||||
|
GAUGE_MAP = {'G1': 14,
|
||||||
|
'G2': 15,
|
||||||
|
'G3': 16,
|
||||||
|
'P1': 20,
|
||||||
|
'P2': 21,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Pressure(OxBase, Readable):
|
||||||
|
|
||||||
|
addr = Property('pressure gauge address', datatype=EnumType(GAUGE_MAP))
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
nr = self.addr.value
|
||||||
|
if self.addr.name.startswith('G'):
|
||||||
|
return self.query(f'R{nr}', 0.1)
|
||||||
|
return self.query(f'R{nr}', 1)
|
||||||
|
|
||||||
|
|
||||||
|
class MixPower(OxBase, Writable):
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('mix power', datatype=FloatRange(0, 0.02, unit='W'))
|
||||||
|
value = Parameter('mix power', datatype=FloatRange(0, 0.02, unit='W'))
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
scale = 10**-(7 - self.io._heater_range)
|
||||||
|
return self.query('R4', scale)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if target:
|
||||||
|
self.command('A1') # on, fixed heater power
|
||||||
|
target = min(0.01999, target)
|
||||||
|
target_nW = str(int(target * 1e9))
|
||||||
|
range_mix = max(1, len(target_nW) - 3)
|
||||||
|
if target_nW >= '2000':
|
||||||
|
range_mix += 1
|
||||||
|
scale = 10**-(10 - range_mix)
|
||||||
|
self.command(f'E{range_mix}')
|
||||||
|
self.change('M', target, scale)
|
||||||
|
else:
|
||||||
|
self.command('A0') # turn off
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if self.io._mix_status:
|
||||||
|
return IDLE, 'on'
|
||||||
|
return IDLE, 'off'
|
||||||
|
|
||||||
|
|
||||||
|
class SorbPower(OxBase, Writable):
|
||||||
|
|
||||||
|
""" heater status:
|
||||||
|
bit 0 still on
|
||||||
|
bit 1 sorb in temperature control (this ctr mode is not used)
|
||||||
|
bit 2 sorb in power control """
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('sorb power', datatype=FloatRange(0, 2, unit='W')) # Werte 0.001, 2
|
||||||
|
writecmd = 'B' # in units of 1mW (range 0000 to 1999)
|
||||||
|
scale = 1e-3
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self.io._heater_status & 6:
|
||||||
|
return self.query('R6', self.scale)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.change('O', self.io._heater_status & 1 | 4 * (target > 0), 1)
|
||||||
|
self.change('B', target, self.scale)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
sorb_status = self.io._heater_status & 6
|
||||||
|
if sorb_status == 2:
|
||||||
|
return WARN, 'sorb in temperature control mode'
|
||||||
|
return IDLE, ('on' if sorb_status else 'off')
|
||||||
|
|
||||||
|
|
||||||
|
class StillPower(OxBase, Writable):
|
||||||
|
|
||||||
|
""" heater status:
|
||||||
|
bit 0 still on
|
||||||
|
bit 1 sorb in temperature control (this ctr mode is not used)
|
||||||
|
bit 2 sorb in power control """
|
||||||
|
|
||||||
|
ioClass = IGH_IO
|
||||||
|
|
||||||
|
target = Parameter('still power', datatype=FloatRange(0, 0.2, unit='W'))
|
||||||
|
readcmd = 'R5'
|
||||||
|
writecmd = 'S' # in units of 0.1mW (range 0000 to 1999)
|
||||||
|
scale = 1e-4
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self.io._heater_status & 1:
|
||||||
|
return self.query('R5', self.scale)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.change('O', self.io._heater_status & 6 | (target > 0), 1)
|
||||||
|
self.change('S', target, self.scale)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
sorb_status = self.io._heater_status & 1
|
||||||
|
return IDLE, ('on' if sorb_status else 'off')
|
||||||
|
|
||||||
|
|
||||||
|
class N2Sensor(Readable):
|
||||||
|
|
||||||
|
value = Parameter(datatype=FloatRange(unit='K'))
|
||||||
|
|
||||||
|
|
||||||
|
class Pump(Valve):
|
||||||
|
|
||||||
|
value = Parameter('state of valve (open or close)', datatype=EnumType(on=1, off=0))
|
||||||
|
target = Parameter('open or close valve', datatype=EnumType(on=1, off=0))
|
||||||
|
upper_LN2 = Attached()
|
||||||
|
lower_LN2 = Attached()
|
||||||
|
PATTERN = re.compile(r'\?\{(\d),(\d+),(\d+)\}')
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
reply = self.communicate('{r}')
|
||||||
|
match = self.PATTERN.match(reply)
|
||||||
|
if match:
|
||||||
|
value, upper_LN2, lower_LN2 = match.groups()
|
||||||
|
self.upper_LN2.value = 0.1 * int(upper_LN2)
|
||||||
|
self.lower_LN2.value = 0.1 * int(lower_LN2)
|
||||||
|
return int(value)
|
||||||
|
raise CommunicationFailedError('bad reply to {r}')
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
# hex -> int -> check if bit in bin(integer) is set at the addr position
|
||||||
|
return bit(self.io._valves, self.addr.value - 1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
# open: 2 * 24, close: 2 * 24 + 1
|
||||||
|
self.change('P', 2 * self.addr.value + 1 - target, 1)
|
||||||
|
self.value = target
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if self.target and not self.value:
|
||||||
|
return WARN, 'pump switched off'
|
||||||
|
return IDLE, ''
|
||||||
@@ -101,9 +101,8 @@ class PImixin(HasOutputModule, Writable):
|
|||||||
_lastdiff = None
|
_lastdiff = None
|
||||||
_lasttime = 0
|
_lasttime = 0
|
||||||
_get_range = None # a function get output range from output_module
|
_get_range = None # a function get output range from output_module
|
||||||
_overflow = 0
|
_overflow = None # history of overflow (is not zero when integration overflows output range)
|
||||||
_itime_set = None # True: 'itime' was set, False: 'i' was set
|
_itime_set = None # True: 'itime' was set, False: 'i' was set
|
||||||
_history = None
|
|
||||||
__errcnt = 0
|
__errcnt = 0
|
||||||
__inside_poll = False
|
__inside_poll = False
|
||||||
__cache = None
|
__cache = None
|
||||||
@@ -114,6 +113,7 @@ class PImixin(HasOutputModule, Writable):
|
|||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self.__cache = {}
|
self.__cache = {}
|
||||||
|
self._overflow = np.zeros(10)
|
||||||
super().initModule()
|
super().initModule()
|
||||||
if self.output_range != (0, 0): # legacy !
|
if self.output_range != (0, 0): # legacy !
|
||||||
self.output_min, self.output_max = self.output_range
|
self.output_min, self.output_max = self.output_range
|
||||||
@@ -131,13 +131,6 @@ class PImixin(HasOutputModule, Writable):
|
|||||||
self.__cache = {}
|
self.__cache = {}
|
||||||
now = time.time()
|
now = time.time()
|
||||||
value = self.read_value()
|
value = self.read_value()
|
||||||
if self._history is None:
|
|
||||||
# initialize a fixed size array, with fake time axis to avoid errors in np.polyfit
|
|
||||||
self._history = np.array([(now+i, self.value) for i in range(-9,1)])
|
|
||||||
else:
|
|
||||||
# shift fixed size array, and change last point
|
|
||||||
self._history[:-1] = self._history[1:]
|
|
||||||
self._history[-1] = (now, value)
|
|
||||||
if not self.control_active:
|
if not self.control_active:
|
||||||
self._lastdiff = 0
|
self._lastdiff = 0
|
||||||
return
|
return
|
||||||
@@ -150,30 +143,34 @@ class PImixin(HasOutputModule, Writable):
|
|||||||
self._lastdiff = diff
|
self._lastdiff = diff
|
||||||
deltadiff = diff - self._lastdiff
|
deltadiff = diff - self._lastdiff
|
||||||
self._lastdiff = diff
|
self._lastdiff = diff
|
||||||
if diff:
|
|
||||||
ref = self.itime / diff
|
|
||||||
(slope, _), cov = np.polyfit(self._history[:, 0] - now, self._history[:, 1], 1, cov=True)
|
|
||||||
slope_stddev = np.sqrt(max(0, cov[0, 0]))
|
|
||||||
if slope * ref > 1 + 2 * slope_stddev * abs(ref):
|
|
||||||
# extrapolated value will cross target in less than itime
|
|
||||||
if self._overflow:
|
|
||||||
self._overflow = 0
|
|
||||||
self.log.info('clear overflow')
|
|
||||||
|
|
||||||
output, omin, omax = self.cvt2int(out.target)
|
output, omin, omax = self.cvt2int(out.target)
|
||||||
output += self._overflow + (
|
output += self._overflow[-1] + (
|
||||||
self.p * deltadiff +
|
self.p * deltadiff +
|
||||||
self.i * deltat * diff / self.time_scale) / self.input_scale
|
self.i * deltat * diff / self.time_scale) / self.input_scale
|
||||||
if omin <= output <= omax:
|
if omin <= output <= omax:
|
||||||
self._overflow = 0
|
overflow = 0
|
||||||
else:
|
else:
|
||||||
# save overflow for next step
|
# save overflow for next step
|
||||||
if output < omin:
|
if output < omin:
|
||||||
self._overflow = output - omin
|
overflow = output - omin
|
||||||
output = omin
|
output = omin
|
||||||
else:
|
else:
|
||||||
self._overflow = output - omax
|
overflow = output - omax
|
||||||
output = omax
|
output = omax
|
||||||
|
if overflow:
|
||||||
|
# fit a straight line
|
||||||
|
(slope, beg), cov = np.polyfit(range(self._overflow), self._overflow, 1, cov=True)
|
||||||
|
sign = np.copysign(1, overflow)
|
||||||
|
end = beg + slope * len(self._overflow)
|
||||||
|
# reduce the absolute value of overflow by the minimum distance of the fitted
|
||||||
|
# line to zero, with a margin of 3 * stddev
|
||||||
|
shift = max(0, min(overflow * sign, min(beg * sign, end * sign) - 3 * np.sqrt(cov[1, 1]))) * sign
|
||||||
|
if shift:
|
||||||
|
overflow -= shift
|
||||||
|
self._overflow -= shift
|
||||||
|
self._overflow[:-1] = self._overflow[1:]
|
||||||
|
self._overflow[-1] = overflow
|
||||||
out.update_target(self.name, self.cvt2ext(output))
|
out.update_target(self.name, self.cvt2ext(output))
|
||||||
self.__errcnt = 0
|
self.__errcnt = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -187,10 +184,10 @@ class PImixin(HasOutputModule, Writable):
|
|||||||
finally:
|
finally:
|
||||||
self.__inside_poll = False
|
self.__inside_poll = False
|
||||||
self.__cache = {}
|
self.__cache = {}
|
||||||
self.overflow = self._overflow
|
self.overflow = self._overflow[-1]
|
||||||
|
|
||||||
def write_overflow(self, value):
|
def write_overflow(self, value):
|
||||||
self._overflow = value
|
self._overflow.fill(value)
|
||||||
|
|
||||||
def internal_poll(self):
|
def internal_poll(self):
|
||||||
super().doPoll()
|
super().doPoll()
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class Main(Communicator):
|
|||||||
|
|
||||||
class PpmsBase(HasIO, Readable):
|
class PpmsBase(HasIO, Readable):
|
||||||
"""common base for all ppms modules"""
|
"""common base for all ppms modules"""
|
||||||
|
ioClass = Main
|
||||||
value = Parameter(needscfg=False)
|
value = Parameter(needscfg=False)
|
||||||
status = Parameter(datatype=StatusType(Readable, 'DISABLED'), needscfg=False)
|
status = Parameter(datatype=StatusType(Readable, 'DISABLED'), needscfg=False)
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PulseIO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Base(HasIO):
|
class Base(HasIO):
|
||||||
|
ioClass = PulseIO
|
||||||
|
|
||||||
def set_source(self):
|
def set_source(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
63
frappy_psi/sim_dil.py
Normal file
63
frappy_psi/sim_dil.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Anik Stark <anik.stark@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import time
|
||||||
|
from frappy.core import Parameter, EnumType, FloatRange, Writable
|
||||||
|
|
||||||
|
|
||||||
|
class Valve(Writable):
|
||||||
|
|
||||||
|
value = Parameter('state of valve (open or close)', datatype=EnumType(open=1, close=0))
|
||||||
|
target = Parameter('open or close valve', datatype=EnumType(open=1, close=0))
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.value = target
|
||||||
|
|
||||||
|
|
||||||
|
class PulsedValve(Valve):
|
||||||
|
|
||||||
|
delay = Parameter('delay (time valve is open)', FloatRange(unit='s'), readonly=False)
|
||||||
|
_start = 0
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
if target:
|
||||||
|
self._start = time.time()
|
||||||
|
self.setFastPoll(True, 0.01)
|
||||||
|
self.value = target
|
||||||
|
else:
|
||||||
|
self.setFastPoll(False)
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
super().doPoll()
|
||||||
|
if self._start:
|
||||||
|
if time.time() > self._start + self.delay:
|
||||||
|
self.write_target(0)
|
||||||
|
self.value = 0
|
||||||
|
self._start = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(Writable):
|
||||||
|
""" Pressure and motor valve. """
|
||||||
|
|
||||||
|
value = Parameter('sensor value', datatype=FloatRange(), readonly=False)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.value = target
|
||||||
@@ -30,6 +30,7 @@ class IO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Power(HasIO, Readable):
|
class Power(HasIO, Readable):
|
||||||
|
ioClass = IO
|
||||||
value = Parameter(datatype=FloatRange(0,3300,unit='W'))
|
value = Parameter(datatype=FloatRange(0,3300,unit='W'))
|
||||||
voltage = Parameter('voltage', FloatRange(0,8, unit='V'))
|
voltage = Parameter('voltage', FloatRange(0,8, unit='V'))
|
||||||
current = Parameter('current', FloatRange(0,400, unit='A'))
|
current = Parameter('current', FloatRange(0,400, unit='A'))
|
||||||
@@ -41,6 +42,7 @@ class Power(HasIO, Readable):
|
|||||||
|
|
||||||
|
|
||||||
class Output(HasIO, Writable):
|
class Output(HasIO, Writable):
|
||||||
|
ioClass = IO
|
||||||
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
|
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
|
||||||
target = Parameter(datatype=FloatRange(0,100,unit='%'))
|
target = Parameter(datatype=FloatRange(0,100,unit='%'))
|
||||||
mode = Parameter('regulation mode', EnumType(voltage=1, current=2, both=3),
|
mode = Parameter('regulation mode', EnumType(voltage=1, current=2, both=3),
|
||||||
|
|||||||
68
test/test_iocfg.py
Normal file
68
test/test_iocfg.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
from logging import Logger
|
||||||
|
import pytest
|
||||||
|
from frappy.core import StringIO, Module, HasIO
|
||||||
|
from frappy.config import process_file, fix_io_modules, Param
|
||||||
|
|
||||||
|
|
||||||
|
class Mod(HasIO, Module):
|
||||||
|
ioClass = StringIO
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG = """
|
||||||
|
IO('io_a', 'tcp://test.psi.ch:7777', visibility='w--')
|
||||||
|
IO('io_b', 'tcp://test2.psi.ch:8080')
|
||||||
|
|
||||||
|
Mod('mod1', 'test.test_iocfg.Mod', '',
|
||||||
|
io='io_a',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('mod2', 'test.test_iocfg.Mod', '',
|
||||||
|
io='io_b',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('mod3', 'test.test_iocfg.Mod', '',
|
||||||
|
io='io_b',
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('mod, ioname, iocfg', [
|
||||||
|
('mod1', 'io_a', {
|
||||||
|
'cls': 'test.test_iocfg.Mod.ioClass',
|
||||||
|
'description': 'communicator for mod1',
|
||||||
|
'uri': Param('tcp://test.psi.ch:7777'),
|
||||||
|
'visibility': Param('w--')
|
||||||
|
},),
|
||||||
|
('mod2', 'io_b', {
|
||||||
|
'cls': 'test.test_iocfg.Mod.ioClass',
|
||||||
|
'description': 'communicator for mod2, mod3',
|
||||||
|
'uri': Param('tcp://test2.psi.ch:8080'),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
def test_process_file(mod, ioname, iocfg):
|
||||||
|
log = Logger('dummy')
|
||||||
|
config = process_file('<test>',log, CONFIG)
|
||||||
|
fix_io_modules(config, log)
|
||||||
|
assert config[mod]['io'] == {'value': ioname}
|
||||||
|
assert config[ioname] == iocfg
|
||||||
Reference in New Issue
Block a user