3 Commits

Author SHA1 Message Date
1bb869b43e capillary heater: heater is now a writable
The value should show the actual heater power, but
we do not know yet the address. Currently the value
is just equal to the maxheater parameter.
2025-11-27 17:55:52 +01:00
3ede9eb9f4 frappy_psi.logo: revised version after merging capillary heater
- check that dil5 still works!

Change-Id: Ibe98e64088f2f886888af170a1f38d699927eb58
2025-11-27 09:32:37 +01:00
f57400feb9 frappy_psi.picontrol: stop when switching to manual mode
Change-Id: I3ffb9a109fb3b04fbca06f5a72acbfbd19525aae
2025-11-27 07:49:29 +01:00
53 changed files with 691 additions and 5347 deletions

View File

@@ -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,IO additional-builtins=Node,Mod,Param,Command,Group
[BASIC] [BASIC]

View File

@@ -1,45 +0,0 @@
#!/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()

View File

@@ -1,22 +0,0 @@
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',
)

View File

@@ -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='ldmcc05-ts:3006', uri='ldmcc08-ts:3006',
) )
Mod('stickrot', Mod('stickrot',

View File

@@ -1,19 +0,0 @@
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')
)

View File

@@ -1,17 +0,0 @@
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'),
)

View File

@@ -1,100 +0,0 @@
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',
)

View File

@@ -83,38 +83,37 @@ Mod('compressor',
) )
Mod('p2', Mod('p2',
'frappy_psi.logo.Value', 'frappy_psi.logo.Pressure',
'pressure after compressor', 'pressure after compressor',
io = 'logo', io = 'logo',
addr ="VW0", addr ="VW0",
value = Param(unit='mbar'), pollinterval=0.5,
) )
Mod('p1', Mod('p1',
'frappy_psi.logo.Value', 'frappy_psi.logo.Pressure',
'dump pressure', 'dump pressure',
io = 'logo', io = 'logo',
addr ="VW28", addr ="VW28",
value = Param(unit='mbar'), pollinterval=0.5,
) )
Mod('p5', Mod('p5',
'frappy_psi.logo.Value', 'frappy_psi.logo.Pressure',
'pressure after forepump', 'pressure after forepump',
io = 'logo', io = 'logo',
addr ="VW4", addr ="VW4",
value = Param(unit='mbar'), pollinterval = 0.5,
) )
Mod('airpressure', Mod('airpressure',
'frappy_psi.logo.DigitalValue', 'frappy_psi.logo.Comparator',
'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',

94
cfg/gas10ka_cfg.py Normal file
View File

@@ -0,0 +1,94 @@
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',
)

View File

@@ -1,16 +0,0 @@
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',
)

View File

@@ -6,7 +6,11 @@ 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
IO('io', lakeshore_uri) # the communicator (its class will be detected automatically) Mod('io', # the name of the module
'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',

View File

@@ -35,8 +35,8 @@ Mod('ts',
'frappy_psi.parmod.Converging', 'frappy_psi.parmod.Converging',
'test for parmod', 'test for parmod',
unit='K', unit='K',
read='th.value', value_param='th.value',
write='th.setsamp', target_param='th.setsamp',
meaning=['temperature', 20], meaning=['temperature', 20],
settling_time=20, settling_time=20,
tolerance=1, tolerance=1,

View File

@@ -1,195 +1,211 @@
# please edit this file with frappy edit Node('mb11.psi.ch',
'MB11 11 Tesla - 100 mm cryomagnet',
)
doc="""MB11 11 Tesla - 100 mm cryomagnet Mod('itc1',
'frappy_psi.mercury.IO',
'ITC for heat exchanger and pressures',
uri='mb11-ts:3001',
)
after conversion with frappy edit Mod('itc2',
""" 'frappy_psi.mercury.IO',
Node('mb11.psi.ch', doc, 'ITC for neck and nv heaters',
interface='tcp://10767', uri='mb11-ts:3002',
) )
IO('itc1', 'mb11-ts:3001') Mod('ips',
'frappy_psi.mercury.IO',
IO('itc2', 'mb11-ts:3002') 'IPS for magnet and levels',
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, settling_time=60.0,
slot = 'DB5.P1', slot='DB5.P1',
io = 'itc1', io='itc1',
tolerance = 1, tolerance=1.0,
value = Param(unit='mbar_flow'), value=Param(
) 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, settling_time=60.0,
slot = 'DB8.P1', slot='DB8.P1',
io = 'itc1', io='itc1',
tolerance = 1, tolerance=1.0,
value = Param(unit='mbar_flow'), value=Param(
) 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, wait_stable_field=60.0,
target = Param(max=11), target=Param(
persistent_limit = 11.1, max=11.0,
) ),
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, tolerance=1.0,
) )
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, tolerance=1.0,
) )
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',
) )
IO('om_io', 'mb11-ts.psi.ch:3004') Mod('om_io',
'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),
) )

View File

@@ -1,149 +0,0 @@
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,
)

View File

@@ -1,37 +0,0 @@
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',
)

View File

@@ -1,17 +0,0 @@
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),
)

View File

@@ -3,14 +3,10 @@ Configuration File
.. _node configuration: .. _node configuration:
:Node: :Node(equipment_id, description, interface, \*\*kwds):
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:
@@ -22,14 +18,9 @@ Configuration File
.. _mod configuration: .. _mod configuration:
:Mod: :Mod(name, cls, description, \*\*kwds):
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
@@ -46,60 +37,22 @@ Configuration File
.. _param configuration: .. _param configuration:
:Param: :Param(value=<undef>, \*\*kwds):
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: :Command(\*\*kwds):
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`)

View File

@@ -140,4 +140,4 @@ Exception classes
.. automodule:: frappy.errors .. automodule:: frappy.errors
:members: :members:
.. include:: configuration.inc .. include:: configuration.rst

View File

@@ -99,11 +99,16 @@ 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
IO('io', 'serial://COM6:?baudrate=57600+parity=odd+bytesize=7') Mod('io', # the name of the module
'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 io module called 'io' io='io', # refers to above defined module '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
) )
@@ -115,8 +120,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.
But first we have to create the ``io`` module for communication. We have to create the ``io`` module for communication first, with
For this we use an :ref:`IO <io configuration>` section. the ``uri`` as its most important argument.
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

View File

@@ -114,18 +114,14 @@ class Collector:
self.modules = {} self.modules = {}
self.warnings = [] self.warnings = []
def add(self, name, cls, description, **kwds): def add(self, *args, **kwds):
mod = Mod(name, cls, description, **kwds) mod = Mod(*args, **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
@@ -184,37 +180,12 @@ class Include:
exec(compile(filename.read_bytes(), filename, 'exec'), self.namespace) exec(compile(filename.read_bytes(), filename, 'exec'), self.namespace)
def fix_io_modules(cfgdict, log): def process_file(filename, log):
node = cfgdict.pop('node') config_text = filename.read_bytes()
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, 'IO': mods.add_io} 'override': mods.override, 'overrideNode': node.override}
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)
@@ -265,7 +236,6 @@ 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:

View File

@@ -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 value if isinstance(value, str) else repr(value) return self.format_value(value, False)
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(SimpleDataType): class DefaultType(DataType):
"""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

View File

@@ -234,37 +234,22 @@ 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 value between min and max # return median, i.e. clamp the 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 an object given by string in dotted notation (as python would do) """loads a class given by string in dotted notation (as python would do)"""
modname, classname = spec.rsplit('.', 1)
import the specified module and get the specified item from it if modname.startswith('frappy'):
examples: 'frappy_demo.lakeshore.TemperatureSensor', 'frappy.modules.Readable.Status' module = importlib.import_module(modname)
else:
:param spec: a dot-separated list of module names followed by the name of # rarely needed by now....
a class (or any object) and optionally names of attributes module = importlib.import_module('frappy.' + modname)
:return: the object try:
""" return getattr(module, classname)
for maxsplit in range(1, len(spec)): except AttributeError:
# len(spec) is high enough for all cases raise AttributeError('no such class') from None
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):
@@ -492,15 +477,3 @@ 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)

View File

@@ -37,13 +37,6 @@ 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}
@@ -81,15 +74,6 @@ 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) )

View File

@@ -38,6 +38,8 @@ 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 = {}
@@ -175,7 +177,7 @@ class SecNode:
try: try:
getattr(modobj, prop) getattr(modobj, prop)
except SECoPError as e: except SECoPError as e:
if generalConfig.raise_config_errors: if self.raise_config_errors:
raise raise
self.error_count += 1 self.error_count += 1
modobj.logError(e) modobj.logError(e)

View File

@@ -59,8 +59,8 @@ except ImportError:
class Server: class Server:
INTERFACES = { INTERFACES = {
'tcp': 'frappy.protocol.interface.tcp.TCPServer', 'tcp': 'protocol.interface.tcp.TCPServer',
'ws': 'frappy.protocol.interface.ws.WSServer', 'ws': 'protocol.interface.ws.WSServer',
} }
_restart = True _restart = True

View File

@@ -1,948 +0,0 @@
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()

View File

@@ -1,175 +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>
#
# *****************************************************************************
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

View File

@@ -1,499 +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>
#
# *****************************************************************************
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

View File

@@ -1,106 +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>
#
# *****************************************************************************
"""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

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,6 @@ 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
@@ -67,11 +66,10 @@ 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 = Parameter('heater power range', IntRange(0, 3), readonly=False) heater_range = Property('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
@@ -103,7 +101,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 = Parameter('heater power range', IntRange(0, 5)) heater_range = Property('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?')

View File

@@ -45,7 +45,6 @@ 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.

View File

@@ -57,7 +57,6 @@ 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):
@@ -68,7 +67,6 @@ 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)

View File

@@ -68,7 +68,6 @@ 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):

View File

@@ -1,160 +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:
# 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

View File

@@ -0,0 +1,53 @@
# *****************************************************************************
#
# 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()

View File

@@ -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): def __init__(self, modfactory, **kwds):
self.modfactory = modfactory self.modfactory = modfactory
instpath = self.configbase / os.environ['Instrument'] instpath = self.configbase / os.environ['Instrument']
sections = {} sections = {}

View File

@@ -1,260 +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:
# 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': '',
}

290
frappy_psi/ips_classic.py Normal file
View File

@@ -0,0 +1,290 @@
#!/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

View File

@@ -50,7 +50,6 @@ 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)
@@ -108,7 +107,6 @@ 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'))
@@ -155,7 +153,6 @@ 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'))

View File

@@ -22,6 +22,8 @@ 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
@@ -30,14 +32,11 @@ 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, LazyImport from frappy.lib import formatStatusBits
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):
@@ -420,7 +419,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:
np_testing.assert_approx_equal(v1, v2, significant, verbose=False) 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
@@ -465,7 +464,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_module.CalCurve(sensor.calcurve) calcurve = 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

View File

@@ -21,8 +21,9 @@ 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, StringType, \ from frappy.core import Attached, Command, Readable, Parameter, FloatRange, HasIO, Property, \
IDLE, BUSY, WARN, ERROR, Writable, Drivable, BoolType, IntRange, Communicator, StatusType IDLE, BUSY, WARN, ERROR, Writable, Drivable, Communicator
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
@@ -80,11 +81,16 @@ class IO(Communicator):
class LogoMixin(HasIO): class LogoMixin(HasIO):
ioclass = IO ioclass = IO
def get_vm_value(self, vm_address): def get_vm_value(self, addr, scale=None):
return literal_eval(self.io.communicate(vm_address)) if scale is None:
return int(self.io.communicate(addr))
return float(self.io.communicate(addr)) * scale
def set_vm_value(self, vm_address, value): def set_vm_value(self, addr, value, scale=None):
return literal_eval(self.io.communicate(f'{vm_address} {round(value)}')) if scale is None:
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):
@@ -219,195 +225,47 @@ class DelayedActuator(DigitalActuator, Drivable):
self._pulse_end = now + delay self._pulse_end = now + delay
class Value(LogoMixin, Readable): class Sensor(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) return self.get_vm_value(self.addr, self.scale)
def read_status(self): def read_status(self):
return IDLE, '' return IDLE, ''
class DigitalValue(Value): class AnalogOutput(Sensor, Writable):
value = Parameter('airpressure state', datatype=BoolType()) output_addr = Property('VM address output', datatype=StringType(), default='')
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)
# TODO: the following classes are too specific, they have to be moved class Pressure(Sensor):
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
def read_value(self): class Resistor(Sensor):
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')) value = Parameter('resistance', datatype=FloatRange(unit='Ohm'))
def read_value(self):
return self.get_vm_value(self.vm_address)
def read_status(self): class Comparator(LogoMixin, Readable):
return IDLE, '' addr = Property('VM address', datatype=StringType())
scale = Property('scale to multiply with raw integer value',
NoneOr(FloatRange()), default=None)
class HeaterParam(LogoMixin, Writable): value = Parameter('airpressure state', datatype=BoolType())
vm_address = Property('VM address output', datatype=StringType()) threshold = Property('threshold for True', FloatRange())
target = Parameter('Heater target', datatype=IntRange())
value = Parameter('Heater Param', datatype=IntRange())
def read_value(self): def read_value(self):
return self.get_vm_value(self.vm_address) return self.get_vm_value(self.addr, self.scale) > self.threshold
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, ''

View File

@@ -64,8 +64,6 @@ 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():

View File

@@ -61,7 +61,6 @@ 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(
@@ -150,24 +149,10 @@ 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:
@@ -181,15 +166,11 @@ 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))
@@ -354,7 +335,6 @@ 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
@@ -373,7 +353,6 @@ 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

View File

@@ -64,15 +64,9 @@ 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>

View File

@@ -1,715 +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>
# 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, ''

View File

@@ -101,8 +101,9 @@ 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 = None # history of overflow (is not zero when integration overflows output range) _overflow = 0
_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
@@ -113,7 +114,6 @@ 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,6 +131,13 @@ 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
@@ -143,34 +150,30 @@ 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[-1] + ( output += self._overflow + (
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:
overflow = 0 self._overflow = 0
else: else:
# save overflow for next step # save overflow for next step
if output < omin: if output < omin:
overflow = output - omin self._overflow = output - omin
output = omin output = omin
else: else:
overflow = output - omax self._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:
@@ -184,10 +187,10 @@ class PImixin(HasOutputModule, Writable):
finally: finally:
self.__inside_poll = False self.__inside_poll = False
self.__cache = {} self.__cache = {}
self.overflow = self._overflow[-1] self.overflow = self._overflow
def write_overflow(self, value): def write_overflow(self, value):
self._overflow.fill(value) self._overflow = value
def internal_poll(self): def internal_poll(self):
super().doPoll() super().doPoll()

View File

@@ -114,7 +114,6 @@ 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)

View File

@@ -27,7 +27,6 @@ class PulseIO(StringIO):
class Base(HasIO): class Base(HasIO):
ioClass = PulseIO
def set_source(self): def set_source(self):
""" """

View File

@@ -1,63 +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:
# 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

View File

@@ -30,7 +30,6 @@ 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'))
@@ -42,7 +41,6 @@ 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),

View File

@@ -1,68 +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 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