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
82 changed files with 1008 additions and 8403 deletions

View File

@@ -86,7 +86,7 @@ dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# 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]

View File

@@ -1,46 +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.editcurses.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',
'frappy_psi.phytron.PhytronIO',
'dom motor IO',
uri='ldmcc05-ts:3006',
uri='ldmcc08-ts:3006',
)
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,16 +0,0 @@
Node('cp2800_test.psi.ch',
'test CRYOMECH CP-2800 compressor',
'tcp://5000',
)
Mod('io',
'frappy_psi.cp2800.IO',
'cp2800 communication',
uri='ldmse3-ts.psi.ch:3008',
)
Mod('CP2800',
'frappy_psi.cp2800.CP2800',
'all parameters of cp2800',
io='io',
)

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',
'frappy_psi.logo.Value',
'frappy_psi.logo.Pressure',
'pressure after compressor',
io = 'logo',
addr ="VW0",
value = Param(unit='mbar'),
)
pollinterval=0.5,
)
Mod('p1',
'frappy_psi.logo.Value',
'frappy_psi.logo.Pressure',
'dump pressure',
io = 'logo',
addr ="VW28",
value = Param(unit='mbar'),
)
pollinterval=0.5,
)
Mod('p5',
'frappy_psi.logo.Value',
'frappy_psi.logo.Pressure',
'pressure after forepump',
io = 'logo',
addr ="VW4",
value = Param(unit='mbar'),
)
pollinterval = 0.5,
)
Mod('airpressure',
'frappy_psi.logo.DigitalValue',
'frappy_psi.logo.Comparator',
'Airpressure state',
io = 'logo',
addr ="V1024.7",
)
threshold = 500,
pollinterval = 0.5,
)
Mod('io_pfeiffer',
'frappy_psi.pfeiffer_new.PfeifferProtocol',

View File

@@ -1,17 +0,0 @@
Node('epc8210_test.test',
'test epc8210 (power line switch) driver',
'tcp://5000',
)
Mod('io',
'frappy_psi.epc8210.IO',
'epc8210 communication',
uri='jtccr-ts:3007',
)
Mod('switch',
'frappy_psi.epc8210.Switch',
'epc8210 power line switch',
io='io',
addr=8,
)

View File

@@ -1,17 +0,0 @@
Node('fungen_test.psi.ch',
'test function generator (Agilent 33210A)',
'tcp://5005',
)
Mod('io',
'frappy_psi.fungen.IO',
'fungen communication',
uri='A-33210A-12987.psi.ch:5025',
#uri="129.129.138.93:5025"
)
Mod('Frequency',
'frappy_psi.fungen.Frequency',
'frequency of fungen',
io='io',
)

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

@@ -1,39 +0,0 @@
Node('leidenghs_test.test',
'test leiden GHS driver',
'tcp://5000',
)
Mod('io',
'frappy_psi.leidenghs.IO',
'leiden GHS communication',
uri='serial:///dev/tty.usbserial-14330?baudrate=9600',
)
Mod('A8',
'frappy_psi.leidenghs.Valve',
'test valve A8',
io='io',
key='A8',
)
Mod('P1',
'frappy_psi.leidenghs.Pressure',
'test pressure P1',
io='io',
addr='P1',
)
Mod('P2',
'frappy_psi.leidenghs.Pressure',
'test pressure P2',
io='io',
addr='P2',
)
Mod('P6',
'frappy_psi.leidenghs.PressureLimit',
'test pressure with limits P6',
io='io',
addr='P6',
offset=0,
)

View File

@@ -6,7 +6,11 @@ lakeshore_uri = environ.get('LS_URI', 'tcp://<host>:7777')
Node('example_cryo.psi.ch', # a globally unique identification
'this is an example cryostat for the Frappy tutorial', # describes the node
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',
'frappy_demo.lakeshore.TemperatureLoop',
'Sample Temperature',

View File

@@ -35,8 +35,8 @@ Mod('ts',
'frappy_psi.parmod.Converging',
'test for parmod',
unit='K',
read='th.value',
write='th.setsamp',
value_param='th.value',
target_param='th.setsamp',
meaning=['temperature', 20],
settling_time=20,
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
"""
Node('mb11.psi.ch', doc,
interface='tcp://10767',
)
Mod('itc2',
'frappy_psi.mercury.IO',
'ITC for neck and nv heaters',
uri='mb11-ts:3002',
)
IO('itc1', 'mb11-ts:3001')
IO('itc2', 'mb11-ts:3002')
IO('ips', 'mb11-ts:3003')
Mod('ips',
'frappy_psi.mercury.IO',
'IPS for magnet and levels',
uri='mb11-ts:3003',
)
Mod('T_stat',
'frappy_psi.mercury.TemperatureAutoFlow',
'static heat exchanger temperature',
meaning = ('temperature_regulation', 27),
output_module = 'htr_stat',
needle_valve = 'p_stat',
slot = 'DB6.T1',
io = 'itc1',
tolerance = 0.1,
flowpars = ((1, 5), (2, 20)),
)
meaning=['temperature_regulation', 27],
output_module='htr_stat',
needle_valve='p_stat',
slot='DB6.T1',
io='itc1',
tolerance=0.1,
flowpars=((1,5), (2, 20)),
)
Mod('htr_stat',
'frappy_psi.mercury.HeaterOutput',
'static heat exchanger heater',
slot = 'DB1.H1',
io = 'itc1',
)
slot='DB1.H1',
io='itc1',
)
Mod('p_stat',
'frappy_psi.mercury.PressureLoop',
'static needle valve pressure',
output_module = 'pos_stat',
settling_time = 60,
slot = 'DB5.P1',
io = 'itc1',
tolerance = 1,
value = Param(unit='mbar_flow'),
)
output_module='pos_stat',
settling_time=60.0,
slot='DB5.P1',
io='itc1',
tolerance=1.0,
value=Param(
unit='mbar_flow',
),
)
Mod('pos_stat',
'frappy_psi.mercury.ValvePos',
'static needle valve position',
slot = 'DB5.P1,DB3.G1',
io = 'itc1',
)
slot='DB5.P1,DB3.G1',
io='itc1',
)
Mod('T_dyn',
'frappy_psi.mercury.TemperatureAutoFlow',
'dynamic heat exchanger temperature',
output_module = 'htr_dyn',
needle_valve = 'p_dyn',
slot = 'DB7.T1',
io = 'itc1',
tolerance = 0.1,
)
output_module='htr_dyn',
needle_valve='p_dyn',
slot='DB7.T1',
io='itc1',
tolerance=0.1,
)
Mod('htr_dyn',
'frappy_psi.mercury.HeaterOutput',
'dynamic heat exchanger heater',
slot = 'DB2.H1',
io = 'itc1',
)
slot='DB2.H1',
io='itc1',
)
Mod('p_dyn',
'frappy_psi.mercury.PressureLoop',
'dynamic needle valve pressure',
output_module = 'pos_dyn',
settling_time = 60,
slot = 'DB8.P1',
io = 'itc1',
tolerance = 1,
value = Param(unit='mbar_flow'),
)
output_module='pos_dyn',
settling_time=60.0,
slot='DB8.P1',
io='itc1',
tolerance=1.0,
value=Param(
unit='mbar_flow',
),
)
Mod('pos_dyn',
'frappy_psi.mercury.ValvePos',
'dynamic needle valve position',
slot = 'DB8.P1,DB4.G1',
io = 'itc1',
)
slot='DB8.P1,DB4.G1',
io='itc1',
)
Mod('mf',
'frappy_psi.ips_mercury.Field',
'magnetic field',
slot = 'GRPZ',
io = 'ips',
tolerance = 0.001,
wait_stable_field = 60,
target = Param(max=11),
persistent_limit = 11.1,
)
slot='GRPZ',
io='ips',
tolerance=0.001,
wait_stable_field=60.0,
target=Param(
max=11.0,
),
persistent_limit=11.1,
)
Mod('lev',
'frappy_psi.mercury.HeLevel',
'LHe level',
slot = 'DB1.L1',
io = 'ips',
)
slot='DB1.L1',
io='ips',
)
Mod('n2lev',
'frappy_psi.mercury.N2Level',
'LN2 level',
slot = 'DB1.L1',
io = 'ips',
)
slot='DB1.L1',
io='ips',
)
Mod('T_neck1',
'frappy_psi.mercury.TemperatureLoop',
'neck heater 1 temperature',
output_module = 'htr_neck1',
slot = 'MB1.T1',
io = 'itc2',
tolerance = 1,
)
output_module='htr_neck1',
slot='MB1.T1',
io='itc2',
tolerance=1.0,
)
Mod('htr_neck1',
'frappy_psi.mercury.HeaterOutput',
'neck heater 1 power',
slot = 'MB0.H1',
io = 'itc2',
)
slot='MB0.H1',
io='itc2',
)
Mod('T_neck2',
'frappy_psi.mercury.TemperatureLoop',
'neck heater 2 temperature',
output_module = 'htr_neck2',
slot = 'DB6.T1',
io = 'itc2',
tolerance = 1,
)
output_module='htr_neck2',
slot='DB6.T1',
io='itc2',
tolerance=1.0,
)
Mod('htr_neck2',
'frappy_psi.mercury.HeaterOutput',
'neck heater 2 power',
slot = 'DB1.H1',
io = 'itc2',
)
slot='DB1.H1',
io='itc2',
)
Mod('T_nvs',
'frappy_psi.mercury.TemperatureLoop',
'static needle valve temperature',
output_module = 'htr_nvs',
slot = 'DB7.T1',
io = 'itc2',
tolerance = 0.1,
)
output_module='htr_nvs',
slot='DB7.T1',
io='itc2',
tolerance=0.1,
)
Mod('htr_nvs',
'frappy_psi.mercury.HeaterOutput',
'static needle valve heater power',
slot = 'DB2.H1',
io = 'itc2',
)
slot='DB2.H1',
io='itc2',
)
Mod('T_nvd',
'frappy_psi.mercury.TemperatureLoop',
'dynamic needle valve heater temperature',
output_module = 'htr_nvd',
slot = 'DB8.T1',
io = 'itc2',
tolerance = 0.1,
)
output_module='htr_nvd',
slot='DB8.T1',
io='itc2',
tolerance=0.1,
)
Mod('htr_nvd',
'frappy_psi.mercury.HeaterOutput',
'dynamic needle valve heater power',
slot = 'DB3.H1',
io = 'itc2',
)
slot='DB3.H1',
io='itc2',
)
Mod('T_coil',
'frappy_psi.mercury.TemperatureSensor',
'coil temperature',
slot = 'MB1.T1',
io = 'ips',
)
slot='MB1.T1',
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',
'frappy_psi.phytron.Motor',
'stick rotation, typically used for omega',
io = 'om_io',
target_min = '-360',
target_max = '360',
encoder_mode = 'NO',
target = Param(min=-360, max=360),
)
io='om_io',
target_min=-360,
target_max=360,
encoder_mode='NO',
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,25 +0,0 @@
# please edit this file with bin/frappy-edit
doc="""super frappy
server to keep track of secop servers on an instrument
"""
Node('superfrappy.psi.ch', doc,
interface='tcp://5001',
)
IO('nicosio', 'localhost:14869')
Mod('local',
'frappy_psi.superfrappy.SuperFrappy',
'this instrument',
instrument = 'zebra',
main_port = 15101,
stick_port = 15201,
io = 'nicosio',
wrapperdir = '/home/linse/cfgfrappy',
setupdir = '/home/linse/setups',
cfgdirs = '/sq_sw/linse/frappycfg:/sq_sw/linse/frappy_zebra/cfg',
target = [],
)

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:
:Node(equipment_id, description, interface, \*\*kwds):
Specify the SEC-node properties.
.. code::
Node(equipment_id, description, interface, **kwds):
The arguments are SECoP node properties and additional internal node configurations
:Parameters:
@@ -22,14 +18,9 @@ Configuration File
.. _mod configuration:
:Mod:
:Mod(name, cls, description, \*\*kwds):
Create a SECoP module.
.. code::
Mod(name, cls, description, **kwds)
Keyworded argument matching a parameter name are used to configure
the initial value of a parameter. For configuring the parameter properties
the value must be an instance of **Param**, using the keyworded arguments
@@ -46,60 +37,22 @@ Configuration File
.. _param configuration:
:Param:
:Param(value=<undef>, \*\*kwds):
Configure a parameter
.. code::
Param(value=<undef>, **kwds):
:Parameters:
- **value** - if given, the initial value of the parameter
- **kwds** - parameter or datatype SECoP properties (see :class:`frappy.param.Parameter`
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:
:Command(\*\*kwds):
Configure a command
.. code::
Command(**kwds)
:Parameters:
- **kwds** - command SECoP properties (see :class:`frappy.param.Commands`)

View File

@@ -140,4 +140,4 @@ Exception classes
.. automodule:: frappy.errors
: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
'this is an example cryostat for the Frappy tutorial', # describes the node
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',
'frappy_psi.lakeshore.TemperatureSensor',
'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
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.
Then for each module a :ref:`Mod <mod configuration>` section follows.
But first we have to create the ``io`` module for communication.
For this we use an :ref:`IO <io configuration>` section.
We have to create the ``io`` module for communication first, with
the ``uri`` as its most important argument.
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
``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.warnings = []
def add(self, name, cls, description, **kwds):
mod = Mod(name, cls, description, **kwds)
def add(self, *args, **kwds):
mod = Mod(*args, **kwds)
name = mod.pop('name')
if name in self.modules:
self.warnings.append(f'duplicate module {name} overrides previous')
self.modules[name] = 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):
"""override properties/parameters of previously configured modules
@@ -184,37 +180,12 @@ class Include:
exec(compile(filename.read_bytes(), filename, 'exec'), self.namespace)
def fix_io_modules(cfgdict, log):
node = cfgdict.pop('node')
io_modules = {k: [] for k, v in cfgdict.items() if v.get('cls') == '<auto>'}
for modname, modcfg in cfgdict.items():
ioname = modcfg.get('io', {}).get('value')
if ioname:
iocfg = cfgdict.get(ioname)
if iocfg:
referring_modules = io_modules.get(ioname)
if referring_modules is not None:
iocfg['cls'] = f"{modcfg['cls']}.ioClass"
referring_modules.append(modname)
# fix description:
for ioname, referring_modules in io_modules.items():
if referring_modules:
if not cfgdict[ioname]['description']:
cfgdict[ioname]['description'] = f"communicator for {', '.join(k for k in referring_modules)}"
else:
log.warning('remove unused io module %r', ioname)
cfgdict.pop(ioname)
cfgdict['node'] = node
def process_file(filename, log, config_text=None):
if config_text is None:
config_text = filename.read_bytes()
def process_file(filename, log):
config_text = filename.read_bytes()
node = NodeCollector()
mods = Collector()
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)
# pylint: disable=exec-used
exec(compile(config_text, filename, 'exec'), ns)
@@ -265,7 +236,6 @@ def load_config(cfgfiles, log):
filename = to_config_path(str(cfgfile), log)
log.debug('Parsing config file %s...', filename)
cfg = process_file(filename, log)
fix_io_modules(cfg, log)
if config:
config.merge_modules(cfg)
else:

View File

@@ -99,7 +99,7 @@ class SimpleDataType(HasProperties):
- StringType: the bare string 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):
"""if needed, reformat value for transport"""
@@ -1132,7 +1132,7 @@ class CommandType(DataType):
# internally used datatypes (i.e. only for programming the SEC-node)
class DefaultType(SimpleDataType):
class DefaultType(DataType):
"""datatype used as default for parameters
needs some minimal interface to avoid errors when

View File

@@ -1,958 +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 sys
import os
import time
from pathlib import Path
from psutil import pid_exists
import frappy
from frappy.errors import ConfigError
from frappy.lib import generalConfig
from frappy.lib import formatExtendedTraceback
from frappy.config import to_config_path
from frappy.editcurses.configdata import Value, cfgdata_to_py, cfgdata_from_py, \
get_datatype, site, ModuleClass, stringtype, class_completion, recommended_prs, \
make_value, ModuleNameCompletion, Module
from frappy.io import IOBase
import frappy.editcurses.terminalgui as tg
from frappy.editcurses.terminalgui import Main, MenuItem, TextEdit, PushButton, \
ModalDialog
from frappy.editcurses.screenwriter import 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))
class TopWidget:
parent_cls = Main
def handle(self, main):
key = super().handle(main)
if key == KEY.LEFT:
main.toggle_detailed()
return None
return key
class Child(tg.Widget):
"""child widget of NodeWidget ot ModuleWidget"""
parent = TopWidget
def get_name(self):
raise NotImplementedError
def collect(self, result):
pass
def check_data(self):
pass
def is_valid(self):
return True
class HasValue(Child):
clsobj = None
def get_name(self):
raise NotImplementedError
def init_value_widget(self, parent, valobj):
self.init_parent(parent)
self.valobj = valobj
if isinstance(valobj.completion, ModuleNameCompletion):
valobj.completion.get_names = self.get_module_list
def validate(self, strvalue, main=None):
pname = self.get_name()
valobj = self.valobj
try:
if pname != 'cls':
if self.clsobj != self.parent.clsobj:
self.clsobj = self.parent.clsobj
self.valobj = make_value(self.get_name(), self.clsobj, valobj.value)
if isinstance(self.valobj.completion, ModuleNameCompletion):
valobj.completion.get_names = self.get_module_list
self.valobj.validate_from_string(strvalue)
self.error = None
except Exception as e:
self.error = str(e)
if main and valobj != self.valobj:
main.touch()
return valobj.strvalue
def get_module_list(self, basecls):
assert isinstance(self.valobj.completion, ModuleNameCompletion)
module = self.parent
main = module.parent
return main.get_module_list(basecls, module.get_name())
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
error = 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)
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
pname, dot, prop = name.partition('.')
if pname.isidentifier() and (prop.isidentifier or dot == ''):
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, result):
"""collect data"""
name = self.get_name()
if name:
result[name] = self.valobj
def draw(self, wr, in_focus=False):
super().draw(wr, in_focus)
valobj = self.valobj
if valobj.strvalue == '' and valobj.default is not None:
default = valobj.datatype.to_string(valobj.default)
if not in_focus:
wr.dim(default)
return
if self.error:
wr.norm(' ')
wr.error(self.error)
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, result):
self.valobj.set_value(self.value)
result[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
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}.', ModuleClass),
'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):
return self.widget_dict[key].valobj.strvalue
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, strvalue):
if not strvalue:
self.strvalue = self.value = ''
raise ValueError('empty name')
if strvalue != self.value and strvalue in self.main.modules:
self.strvalue = self.value = ''
raise ValueError(f'duplicate name {strvalue!r}')
self.value = self.strvalue = strvalue
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 items', 'e', self.purge_prs),
MenuItem('add configurable items', 'a', self.complete_prs),
MenuItem('cut module', KEY.CUT, parent.cut_module),
]
self.configure_class(modulecfg.get('cls'))
for pname, valobj in modulecfg.items():
self.add_widget(pname, 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 get_class(self):
clsvalue = self.widget_dict['cls'].valobj
clsvalue.set_from_string(clsvalue.strvalue)
return self.clsobj or Module
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() if main.detailed else 1
def check_data(self):
"""check clsobj is valid and check all params and props"""
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, ''))
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() in self.fixed_names or 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 get_class(self):
return IOBase
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.LEFT, 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, strvalue):
try:
self.main.set_node_name(strvalue)
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 pname, valobj in nodecfg.items():
if pname == 'doc':
docwidget = DocWidget(self, pname, valobj)
self.widgets.append(docwidget)
self.widget_dict['doc'] = docwidget
else:
self.add_widget(pname, 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
return False
def focus_row(self, to_focus):
main = self.parent
if main.detailed:
return super().focus_row(to_focus)
height = 0
for widget in 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
class EditorMain(Main):
name = 'Main'
detailed = False
tmppath = None
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):
try:
self.titlebar = tg.TitleBar('Frappy Cfg Editor')
super().__init__([], [self.titlebar], [tg.StatusBar(self)],
help_file=Path(frappy.__file__).parents[1] / 'resources/editcurses_help.txt')
# 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(encoding='ascii')
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.modules = {} # dict <module name> of <module class>
self.init_from_content(self.filecontent)
self.module_clipboard = {}
self.pr_clipboard = {}
except Exception:
print(formatExtendedTraceback())
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 get_module_list(self, basecls, ownname):
"""get module list for Attached.
filter by basecls ad exclude own name
"""
return [w.get_name() for w in self.widgets
if w.get_name() != ownname and isinstance(w, ModuleWidget)
and issubclass(w.get_class(), basecls)]
def toggle_detailed(self):
self.detailed = not self.detailed
self.offset = None # recalculate offset from screen pos
self.status(None)
def get_key(self, timeout=None):
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:
# may need refresh as contents may be built in background
continue
else:
key = super().get_key(timeout)
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.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, init_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(encoding='utf-8').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(encoding='utf-8')
self.add_version(filecontent, get_timestamp(self.tmppath))
except FileNotFoundError:
pass
if init_cfgpath:
cfgpaths = [init_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(encoding='utf-8')
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', encoding='utf-8') 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
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, encoding='utf-8')
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', encoding='ascii') 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(encoding='ascii'))
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 BaseException:
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__":
generalConfig.init()
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,651 +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 inspect
import socket
from pathlib import Path
from ast import literal_eval
from importlib import import_module
import frappy
from frappy.lib.comparestring import compare
from frappy.config import process_file, Node
from frappy.core import Module, Parameter, Attached
from frappy.datatypes import DataType, EnumType
from frappy.properties import Property, UNSET
HEADER = "# please edit this file with bin/frappy-edit"
class Site:
base = Path(frappy.__file__).parent.parent
default_interface = 'tcp://10767'
domain = None
frappy_subdir = None
equipment_postfix = 'your.postfix'
# for individual sites
def __init__(self):
self.packages = [v.name for v in self.base.glob('frappy_*')]
if self.frappy_subdir:
try: # own subdir should be first
self.packages.remove(self.frappy_subdir)
self.packages.insert(0, self.frappy_subdir)
except ValueError:
pass
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 as e:
raise ValueError('this is no python value') from e
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 or '' # convert None to empty string
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
default = None
value = None
completion = None
def __init__(self, value, datatype=None, error=None, pr=None,
from_string=False, callback=None):
if value is None:
raise ValueError(datatype)
self.datatype = datatype
if isinstance(pr, Property):
if pr.value is UNSET:
self.default = None if pr.default is UNSET else pr.default
else:
self.default = pr.value
elif isinstance(pr, Parameter):
self.default = pr.default if pr.value is None else pr.value
if isinstance(datatype, EnumType):
self.completion = NameCompletion([v.name for v in datatype._enum.members])
elif isinstance(pr, Attached):
self.completion = ModuleNameCompletion(pr.basecls)
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): # pylint: disable=method-hidden
"""to be overridden"""
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:
pass
return repr(self.strvalue)
def __eq__(self, other):
return (self.value, self.error, self.strvalue
) == (other.value, other.error, other.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: datatype, error or None, property or parameter or None
"""
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, propobj
elif prop:
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not a parameter'
else:
return prop_param.datatype, None, prop_param
# return prop_param.datatype, None, 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, None
return nonstringtype, error, None
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):
def __call__(self, value):
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
def from_string(self, text):
return self.validate(text)
def to_string(self, value):
value = self.validate(value)
return f'{value.__module__}.{value.__qualname__}'
def format_value(self, value, unit=None):
result = repr(self.to_string(value))
if '<' in result:
raise ValueError(result, value)
return result
def compatible(self, other):
raise NotImplementedError
def export_datatype(self):
raise NotImplementedError
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 pname, valobj in kwds.items():
param, _, prop = pname.partition('.')
paramdict.setdefault(param, {})[prop or 'value'] = valobj
for pname, 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'{pname} = {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"{pname} = 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.equipment_postfix}'
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']
except Exception as e:
logger.info('error %r when checking io for %r', e, modname)
continue
try:
ioclass = f"{modcls}.ioClass"
if module_class.validate(iomodcls) != module_class.validate(ioclass):
continue
iomodcfg['cls'] = '<auto>'
iomodcfg.pop('description', None)
iodict[ioname] = iomodcfg
except Exception:
iomod, iocls = iomodcls.rsplit('.', 1)
mod = modcls.rsplit('.', 1)[0]
if mod == iomod:
iomodcls = iocls
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
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
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):
checker = ClassChecker(cls)
if checker.error or not checker.clsobj:
return {}
cls = checker.clsobj
result = {}
for pname in cls.configurables:
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 incomplete element"""
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:
def __init__(self, names):
self.names = names
def __call__(self, value):
if value in self.names:
return len(value), []
return 0, [value] + list(get_suggested(value, self.names))
class ModuleNameCompletion:
def __init__(self, basecls):
self.basecls = basecls
def get_names(self, basecls):
return []
def __call__(self, value):
names = self.get_names(self.basecls)
if value in names:
return len(value), []
return 0, [value] + list(get_suggested(value, names))
class SitePSI(Site):
domain = '.psi.ch'
frappy_subdir = 'frappy_psi'
equipment_postfix = 'psi.ch'
hostname = socket.getfqdn()
for sitecls in [SitePSI]: # add here other sites
if hostname.endswith(sitecls.domain):
break
site = sitecls()

View File

@@ -1,400 +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 sys
import curses
import threading
from select import select
class KEY:
"""helper class for keys
converts all keys used into an instance of Key
"""
# unfortunately, we can not use some ctrl keys, as they already have a meaning:
# ^H: backspace, ^I: tab, ^M: ret, ^S/^Q: flow control
ESC = 27
TAB = 9
DEL = 127
RETURN = 13
QUIT = '^q'
BEG_LINE = '^a'
END_LINE = '^e'
MENU = '^x'
CUT = '^k'
PASTE = '^v'
HELP = '^g'
UNHANDLED = -1
GOTO_MAIN = -2
GO_UP = -3
UP = None # get curses.KEY_UP
DOWN = None
LEFT = None
RIGHT = None
ENTER = None
bynumber = {k: chr(k) for k in range(32, 127)}
byname = {}
@classmethod
def init(cls):
for name in dir(cls):
if name.isupper():
cls.add_key(name, getattr(cls, name))
for base in cls.__mro__:
for name in getattr(base, 'from_curses', ()):
cls.add_key(name, getattr(curses, f'KEY_{name}'))
@classmethod
def add_key(cls, name, nr):
if isinstance(nr, str):
assert nr[0] == '^'
nr = ord(nr[1]) & 0x1f
elif nr is None:
nr = getattr(curses, f'KEY_{name}')
cls.byname[name] = cls.bynumber[nr] = key = Key(name, nr)
setattr(cls, name, key)
@classmethod
def add(cls, **kwds):
for name, nr in kwds.items():
cls.add_key(name, nr)
class Key(int):
def __new__(cls, name, nr):
if isinstance(nr, str):
if nr.startswith('^'):
nr = ord(nr[1]) & 0x1f
key = super().__new__(cls, nr)
key.name = name
return key
def short(self):
"""as __repr__, but ctrl keys are translated to ^<letter>"""
if 0 <= self < 32:
if 0 < self <= 26:
return f'^{chr(96 + self)}'
return f'^{chr(64 + self)}'
return self.name
def __repr__(self):
"""name by function"""
return self.name
class Screen:
def __init__(self):
self.pairs = {}
self.colors = {}
self.nextcolor = 16
self.lock = threading.RLock()
def init(self):
self.scr = curses.initscr()
try:
self.init_colors(self.scr)
self.hascolors = True
except Exception:
self.hascolors = False
curses.noecho()
curses.raw() # disable ctrl-C interrupt and ctrl-Q/S flow control
curses.nonl() # accept ctrl-J
self.scr.keypad(True)
KEY.init()
def finish(self):
self.scr.keypad(False)
curses.echo()
curses.nocbreak()
curses.endwin()
def make_color(self, rgb):
if isinstance(rgb, int):
# a color number was given
return rgb
idx = self.colors.get(rgb)
if idx is None:
idx = self.nextcolor
self.nextcolor += 1
self.colors[idx] = rgb
curses.init_color(idx, *rgb)
return idx
def make_pair(self, fg, bg):
fg = self.make_color(fg)
bg = self.make_color(bg)
pair = self.pairs.get((fg, bg))
if pair is not None:
return pair
idx = len(self.pairs) + 1
curses.init_pair(idx, fg, bg)
self.pairs[(fg, bg)] = pair = curses.color_pair(idx)
return pair
def init_colors(self, stdscr):
curses.start_color()
for nr in range(self.nextcolor):
rgb = curses.color_content(nr)
self.colors.setdefault(rgb, nr)
black = self.make_color((0, 0, 0))
dim_white = (680, 680, 680)
stdscr.bkgd(' ', self.make_pair(black, dim_white))
bright_white = 1000, 1000, 1000
self.menustyle = self.brightstyle = self.make_pair(black, bright_white)
red = self.make_color((680, 0, 0))
self.errorstyle = self.make_pair(red, dim_white)
self.errorflag = ''
very_light_blue = (800, 900, 1000)
self.highstyle = self.make_pair(black, very_light_blue)
light_white = (800, 800, 800)
self.buttonstyle = self.make_pair(black, light_white)
light_green = 0, 1000, 0
self.querystyle = self.make_pair(black, light_green)
yellow = 1000, 1000, 0
self.warnstyle = self.make_pair(black, yellow)
grey = 400, 400, 400
self.dimstyle = self.make_pair(grey, dim_white)
def get_key(self, timeout=None):
if timeout is None:
key = self.scr.getch()
else:
self.scr.refresh()
if select([sys.stdin], [], [], timeout)[0]:
key = self.scr.getch()
else:
return None
key = KEY.bynumber.get(key, key)
return key
def set_cursor(self, flag, *cursor_pos):
if flag:
self.scr.move(*cursor_pos)
curses.curs_set(1)
else:
curses.curs_set(0)
class BaseWriter:
"""base for writer. does nothing else than keeping track of position"""
errorflag = '! '
def __init__(self, screen):
self.width = None
self.screen = screen
self.nextrow = 0
self.left = 0
def move(self, row, col):
self.row = self.nextrow = row
self.col = self.left = col
def startrow(self):
self.row = self.nextrow
self.col = self.left
self.nextrow = self.row + 1
def norm(self, text, width=0):
self.wr(text, extend_to=width)
def dim(self, text, width=0):
self.wr(text, self.screen.dimstyle, extend_to=width)
def edit(self, text, width, pos):
self.bright(text, width)
def bright(self, text, width=0):
self.wr(text.ljust(width), self.screen.brightstyle, extend_to=width)
def high(self, text, width=0):
self.wr(text.ljust(width), self.screen.highstyle, extend_to=width)
def menu(self, text, width=0):
self.wr(text, self.screen.menustyle, extend_to=width)
def button(self, text, width=0):
self.wr(text, self.screen.buttonstyle, extend_to=width)
def error(self, text, width=0):
self.wr(f'{self.errorflag}{text}', self.screen.errorstyle, extend_to=width)
def write_raw(self, row, text, *attr):
self.col += len(text)
def wr(self, text, *attr, extend_to=0):
"""write text on screen
:param text: the text
:param attr: attributes
:param extend_to: extend_to >= 0: fill up to given value
extend_to < 0: extend to right margin + extend_to
"""
if self.width:
limit = self.width - self.col
if limit <= 0:
return
if extend_to < 0:
limit += extend_to
text = text.ljust(limit)
else: # elif extend_to >= limit
text = text.ljust(extend_to)[:limit]
else:
text = text.ljust(extend_to)
self.write_raw(self.row, text, *attr)
class Writer(BaseWriter):
highstyle = curses.A_REVERSE
buttonstyle = curses.A_BOLD
barstyle = curses.A_REVERSE
brightstyle = menustyle = 0
errorstyle = 0
errorflag = '! '
newoffset = None
querystyle = curses.A_BOLD
warnstyle = curses.A_REVERSE
dimstyle = 0
leftwidth = 8 # minimum left column width
def __init__(self, screen, main):
super().__init__(screen)
self.scr = screen.scr
self.main = main
self.scr.clear()
self.height, self.width = self.scr.getmaxyx()
self.popup = None
self.top = 0
self.bot = self.height
self.adjust_offset_to = None
self.preferred_window_row = None
self.offset = 0
def set_leftwidth(self, leftwidth):
if leftwidth < 1:
self.leftwidth = int(self.width * leftwidth + 1)
else:
self.leftwidth = leftwidth
def write_raw(self, row, text, *attr):
row = self.row - self.offset # screen row
if self.top <= row < self.bot and self.col < self.width:
try:
self.scr.addstr(row, self.col, text, *attr)
self.col += len(text)
except curses.error:
self.col += len(text)
if self.col < self.width or row < self.height - 1:
raise
def edit(self, text, width, pos):
"""write text and set cursor at given pos
also scroll horizontally if cursor would be outside screen
"""
if pos is not None:
maxwidth = self.width - self.col
scrollrange = len(text) - maxwidth + 1
if scrollrange <= 0 or pos < scrollrange:
offset = 0
else:
offset = min(pos - scrollrange, scrollrange)
text = text[offset:]
if len(text) - offset < maxwidth:
text += ' '
self.set_cursor_pos(pos - offset, True)
self.bright(text, width)
def set_cursor_pos(self, pos=0, visible=False):
self.cursor_visible = visible
self.main.cursor_pos = self.row - self.offset, self.col + pos
def vline(self, row, col, length, top, bottom, left, right, *attr):
"""draw a vertical line
:param row, col: upper start point
:param length: length without clipping
:param top, bottom, left, right: clipping
:return: <start row or None>, <end row or None> (None is returned, when clipped on this corner)
"""
beg = None
end = None
if left <= col < right and row < bottom:
end = row + length
if end >= bottom:
length -= end - bottom
end = None
beg = row
if beg < top:
row = top
length -= top - beg
beg = None
if length > 0:
self.scr.vline(row, col, curses.ACS_VLINE, length, *attr)
return beg, end
def hline(self, row, col, length, top, bottom, left, right, *attr):
"""draw a horizontal line
:param row, col: left start point
:param length: length without clipping
:param top, bottom, left, right: clipping
:return: <start row or None>, <end row or None> (None is returned, when clipped on this corner)
"""
beg = None
end = None
if top <= row < bottom and col < right:
end = col + length
if end >= right:
length -= end - right
end = None
beg = col
if beg < left:
col = left
length -= left - beg
beg = None
if length > 0:
self.scr.hline(row, col, curses.ACS_HLINE, length, *attr)
else:
return None, None
return beg, end
def rectangle(self, row, col, height, width, *attr, **clip):
"""clipped rectangle"""
row = row - self.offset
args = (
max(0, clip.get('top', 0) - self.offset),
min(self.height, clip.get('bottom', self.height + self.offset) - self.offset),
max(0, clip.get('left', 0)),
min(self.width, clip.get('right', self.width))
) + attr
self.vline(row, col, height, *args)
self.vline(row, col + width, height, *args)
left, right = self.hline(row, col, width, *args)
if left is not None:
self.scr.addch(row, left, curses.ACS_ULCORNER, *attr)
if right is not None:
self.scr.addch(row, right, curses.ACS_URCORNER, *attr)
row += height
left, right = self.hline(row, col, width, *args)
if left is not None:
self.scr.addch(row, left, curses.ACS_LLCORNER, *attr)
if right is not None:
self.scr.addch(row, right, curses.ACS_LRCORNER, *attr)

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
class HasIO(Module):
"""Mixin for modules using a communicator"""
io = Attached(Communicator, mandatory=False) # either io or uri must be given
io = Attached(mandatory=False) # either io or uri must be given
uri = Property('uri for automatic creation of the attached communication module',
StringType(), default='')

View File

@@ -234,41 +234,22 @@ def clamp(_min, value, _max):
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!
"""
# 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]
def get_class(spec):
"""loads an object given by string in dotted notation (as python would do)
import the specified module and get the specified item from it
examples: 'frappy_demo.lakeshore.TemperatureSensor', 'frappy.modules.Readable.Status'
:param spec: a dot-separated list of module names followed by the name of
a class (or any object) and optionally names of attributes
:return: the object
"""
error = None
for maxsplit in range(1, len(spec)):
# len(spec) is high enough for all cases
module, *attrs = spec.rsplit('.', maxsplit)
try:
obj = importlib.import_module(module)
break
except ImportError as e:
if error is None:
error = module, e
if '.' in module:
continue
raise
for na, attr in enumerate(attrs):
try:
obj = getattr(obj, attr)
except AttributeError:
if error is not None:
raise ImportError(f'{error[1]} during import {error[0]}') from None
raise AttributeError(f'{".".join(attrs[:na+1])!r} not found in {module!r}') from None
return obj
"""loads a class given by string in dotted notation (as python would do)"""
modname, classname = spec.rsplit('.', 1)
if modname.startswith('frappy'):
module = importlib.import_module(modname)
else:
# rarely needed by now....
module = importlib.import_module('frappy.' + modname)
try:
return getattr(module, classname)
except AttributeError:
raise AttributeError('no such class') from None
def mkthread(func, *args, **kwds):

View File

@@ -1,150 +0,0 @@
#
# (C) 2005, Rob W. W. Hooft (rob@hooft.net)
#
# Comparison algorithm as implemented by Reinhard Schneider and Chris Sander
# for comparison of protein sequences, but implemented to compare two
# ASCII strings. This can be very useful for command interpreters to account
# for mistyped commands (use the routine "compare(s1, s2)" in here to get
# a score for each possible command, and see if one stands out). The comparison
# makes use of a similarity matrix for letters: in the protein case this is
# normally a chemical functionality similarity, for our case this is a matrix
# based on keys next to each other on a US Qwerty keyboard and on "capital
# letters are similar to their lowercase equivalent"
#
# The algorithm does not cut corners: it is sure to find the absolute best
# match between the two strings.
#
# No attempt has been made to make this efficient in time or memory. Time taken
# and memory used is proportional to the product of the length of the input
# strings. Use for strings longer than 25 characters is entirely for your own
# risk.
#
# Use freely, but please attribute when using.
# from http://starship.python.net/crew/hooft/
# How much does it cost to make a hole in one of the strings?
GAPOPENPENALTY = -0.3
# How much does it cost to elongate a hole in one of the strings?
GAPELONGATIONPENALTY = -0.2
# How much alike (0.0-1.0) are small and capital letters?
CAPITALIZESCORE = 0.8
# How much alike (0.0-1.0) are characters next to each other on a (US) keyboard?
NEXTKEYSCORE = 0.6
comparematrix = {}
def _makekeyboardmap():
# different characters score 0.0, equal characters score 1.0
for i in range(33, 126 + 1):
for j in range(33, 126 + 1):
comparematrix[i, j] = 0.0
comparematrix[i, i] = 1.0
# Capital and small letters are CAPITALIZESCORE alike
capdist = ord('A') - ord('a')
for i in range(ord('a'), ord('z') + 1):
comparematrix[i, i + capdist] = CAPITALIZESCORE
comparematrix[i + capdist, i] = CAPITALIZESCORE
# Keyboard layout, add some score for letters that are close together
line1 = '`1234567890-= '
line2 = ' qwertyuiop[] '
line3 = ' asdfghjkl; '
line4 = ' zxcvbnm,./ '
for i in range(len(line1) - 1):
_keyboardneighbour(line1[i], line1[i + 1])
_keyboardneighbour(line2[i], line2[i + 1])
_keyboardneighbour(line3[i], line3[i + 1])
_keyboardneighbour(line4[i], line4[i + 1])
_keyboardneighbour(line1[i], line2[i])
_keyboardneighbour(line2[i], line3[i])
_keyboardneighbour(line3[i], line4[i])
_keyboardneighbour(line1[i], line2[i + 1])
_keyboardneighbour(line2[i], line3[i + 1])
_keyboardneighbour(line3[i], line4[i + 1])
def _keyboardneighbour(c1, c2):
i1 = ord(c1)
i2 = ord(c2)
if 33 <= i1 <= 126 and 33 <= i2 <= 126:
comparematrix[i1, i2] = NEXTKEYSCORE
comparematrix[i2, i1] = NEXTKEYSCORE
_makekeyboardmap()
def compare(s1, s2):
lh = {}
gapped = {}
l1 = len(s1)
l2 = len(s2)
if s1 == s2:
return l1 + 1
# Top left of the matrix is "before the first character" in both directions
lh[1, 1] = 0.0
gapped[1, 1] = False
# Start with a gap in s1
lh[2, 1] = GAPOPENPENALTY
gapped[2, 1] = True
for ii in range(3, l1 + 2):
lh[ii, 1] = lh[ii - 1, 1] + GAPELONGATIONPENALTY
gapped[ii, 1] = True
# Start with a gap in s2
lh[1, 2] = GAPOPENPENALTY
gapped[1, 2] = True
for jj in range(3, l2 + 2):
lh[1, jj] = lh[1, jj - 1] + GAPELONGATIONPENALTY
gapped[1, jj] = True
# The main algorithm: for each point in the matrix decide what the best
# route so far is, by comparing the diagonal route forward with the
# possibility to open or elongate a gap either way.
for jj in range(1, l2 + 1):
for ii in range(1, l1 + 1):
oc1 = ord(s1[ii - 1])
oc2 = ord(s2[jj - 1])
if 33 <= oc1 <= 126 and 33 <= oc2 <= 126:
ld = comparematrix[oc1, oc2]
elif oc1 == oc2:
ld = 1.0
else:
ld = 0.0
if gapped[ii + 1, jj]:
gph = GAPELONGATIONPENALTY
else:
gph = GAPOPENPENALTY
if gapped[ii, jj + 1]:
gpv = GAPELONGATIONPENALTY
else:
gpv = GAPOPENPENALTY
s = lh[ii, jj] + ld
sh = lh[ii + 1, jj] + gph
sv = lh[ii, jj + 1] + gpv
sd = max(sh, sv)
if s >= sd:
lh[ii + 1, jj + 1] = s
gapped[ii + 1, jj + 1] = False
else:
lh[ii + 1, jj + 1] = sd
gapped[ii + 1, jj + 1] = True
# The highest alignment score is in the bottom right corner of the matrix,
# behind the last character in both strings
return lh[l1 + 1, l2 + 1]
def test():
assert compare('test', 'test') == len('test') + 1.0
assert compare('Test', 'test') == len('test') - 1.0 + CAPITALIZESCORE
assert compare('rest', 'test') == len('test') - 1.0 + NEXTKEYSCORE
assert compare('rest', 'crest') == len('rest') + GAPOPENPENALTY

View File

@@ -1,47 +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>
#
# *****************************************************************************
"""interpolation, mainly used for pid tables"""
import numpy as np
class Interpolation(list):
def __init__(self, table, logx=None, logy=None):
"""initialize table
:param table: sequence of tuple (x, y)
:param logx, logy: True/False: whether to apply log for interpolation
None: automatic (choose log when all values are positive)
:return: interpolated y value
"""
table = np.array(sorted(table))
super().__init__(table)
if len(table) == 0:
return
logx = table[0][0] > 0 if logx is None else logx
logy = min(table[:,1]) > 0 if logy is None else logy
self.fwd = np.log if logx else np.array
self.xvalues = self.fwd(table[:,0])
self.rev = np.exp if logy else np.array
self.yvalues = np.log(table[:,1]) if logy else table[:,1]
def __call__(self, x):
return self.rev(np.interp(self.fwd(x), self.xvalues, self.yvalues))

View File

@@ -37,13 +37,6 @@ class MathParser:
ast.Div: op.truediv,
ast.Pow: op.pow,
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.UAdd: lambda a:a}
@@ -81,15 +74,6 @@ class MathParser:
if isinstance(node, ast.BinOp): # evaluate binary operations
method = self._operators2method[type(node.op)]
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
method = self._operators2method[type(node.op)]
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
no suitable configuration on the server
"""
raise_config_errors = False # collect catchable errors instead of raising
def __init__(self, name, logger, options, srv):
self.equipment_id = options.pop('equipment_id', name)
self.nodeprops = {}
@@ -175,7 +177,7 @@ class SecNode:
try:
getattr(modobj, prop)
except SECoPError as e:
if generalConfig.raise_config_errors:
if self.raise_config_errors:
raise
self.error_count += 1
modobj.logError(e)

View File

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

View File

@@ -55,11 +55,9 @@ def read_release_version():
def write_release_version(version):
try:
with RELEASE_VERSION_FILE.open('w', encoding='utf-8') as f:
f.write(f'{version}\n')
except OSError:
pass # no write permission?
with RELEASE_VERSION_FILE.open('w', encoding='utf-8') as f:
f.write(f'{version}\n')
def get_version(abbrev=4):
# determine the version from git and from RELEASE-VERSION

View File

@@ -36,7 +36,6 @@ class LakeshoreIO(StringIO):
class TemperatureSensor(HasIO, Readable):
"""a temperature sensor (generic for different models)"""
ioClass = LakeshoreIO
# internal property to configure the channel
channel = Property('the Lakeshore channel', datatype=StringType())
# 0, 1500 is the allowed range by the LakeShore controller
@@ -67,11 +66,10 @@ class TemperatureSensor(HasIO, Readable):
class TemperatureLoop(TemperatureSensor, Drivable):
ioClass = LakeshoreIO
# lakeshore loop number to be used for this module
loop = Property('lakeshore loop', IntRange(1, 2), default=1)
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)
_driving = False
@@ -103,7 +101,7 @@ class TemperatureLoop(TemperatureSensor, Drivable):
class TemperatureLoop340(TemperatureLoop):
# 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):
self.communicate(f'RANGE {value};RANGE?')

View File

@@ -45,7 +45,6 @@ class SR830_IO(StringIO):
class StanfRes(HasIO, Readable):
ioClass = SR830_IO
def set_par(self, cmd, *args):
"""
Set parameter.

View File

@@ -57,7 +57,6 @@ class IO(StringIO):
class Power(HasIO, Readable):
ioClass = IO
value = Parameter(datatype=FloatRange(0,300,unit='W'))
def read_value(self):
@@ -68,7 +67,6 @@ class Power(HasIO, Readable):
class Output(HasIO, HasControlledBy, Writable):
ioClass = IO
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
target = Parameter(datatype=FloatRange(0,100,unit='%'))
p_value = Parameter('?', datatype=FloatRange(0,100,unit='%'), default=0)

View File

@@ -1,134 +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>
# *****************************************************************************
"""BlueFors gas handling system"""
from frappy.core import StringIO, Readable, Writable, HasIO, Parameter, Property, \
BoolType, FloatRange, IntRange, StringType
from frappy.errors import CommunicationFailedError
class IO(StringIO):
end_of_line = '\n'
identification = [('names', '.*')] # ? TODO ln 7
def doPoll(self):
reply = self.communicate('status')[5:] # reply expected in the shape of 'v1=1,v2=0,...'
v_list = reply.split(',')
valve_dict = {}
for element in v_list:
name, value = element.split('=')
valve_dict[name] = int(value)
class SimpleValve(Readable, HasIO):
ioClass = IO
value = Parameter('state of valve (open/close)', datatype=BoolType())
addr = Property('valve address', datatype=StringType(), default='') # TODO type?
def initModule(self):
super().initModule()
if not self.addr:
self.setProperty('addr', self.name)
def read_value(self):
if self.io.valve_dict is None:
self.io.doPoll()
return self.io.valve_dict[self.addr]
class Valve(SimpleValve, Writable):
ioClass = IO
target = Parameter('target state of valve (open/close)', datatype=BoolType(), readonly=False)
value = Parameter('state of valve (open/close)', datatype=BoolType())
def write_target(self, target):
if self.addr not in ['V15', 'V17', 'V18']:
self.communicate('control 1')
self.communicate('remote 1')
cmd = 'on' if target else 'off'
self.communicate(f'{cmd} {self.addr}')
self.communicate('remote 0')
self.communicate('control 0')
return self.read_value()
# PRESSURE_MAP = {'pivc': 1,
# 'pstill': 2,
# 'pcond': 3,
# 'phep': 4,
# 'pdump': 5,
# 'paux': 6,
# }
class Pressure(Readable, HasIO):
ioClass = IO
value = Parameter('pressure value', dataType=FloatRange(unit='mbar'))
addr = Property('pressure device', datatype=IntRange(1,6))
def read_value(self):
reply, txtval = self.communicate(f'mgstatus {self.addr}').split()
if reply != 'S05:':
raise CommunicationFailedError(f'bad reply: {reply}')
return float(txtval)
class Flow(Readable, HasIO):
ioClass = IO
value = Parameter('flow value', datatype=FloatRange(unit='')) # TODO: unit = ln/min ?
def read_value(self):
reply, txtval = self.communicate('fmstatus').split()
if reply != 'S09:':
raise CommunicationFailedError(f'bad reply: {reply}')
return float(txtval)
# class Turbo_IO(StringIO):
# end_of_line = '\r'
# identification = [('0010031202=?101', '.*')] # TODO: check reply
# class Turbo(Writable, HasIO):
# """Pfeiffer TC400 telegram structure
# command: a2a1a0*0n2n1n0l1l0dnd0c2c1c0
# a2a1a0 address (e.g. 001)
# *0 action: data request 00, control command 10
# n2n1n0 Pfeiffer Vacuum parameter numbers
# l1l0 data length of dddd (has variable length)
# dnd0 data in type concerned (has variable lenght, from n to 0)
# c1c0 checksum (sum of ASCII values from a2 to d0, modulo 256) """
# ioClass = Turbo_IO
# value = Parameter('state of turbo (on/off)', datatype=BoolType())
# target = Parameter('target state (on/off)', datatype=BoolType())
# dev_addr = Property('device address', datatype=IntRange(), default=001)
# def write_target(self, target):
# cmd = f'{self.dev_addr}10010'
# ctr_sum = sum(ord(c) for c in cmd)
# self.communicate()

View File

@@ -68,7 +68,6 @@ class BridgeIO(StringIO):
class Base(HasIO):
ioClass = BridgeIO
port = Property('modules port', IntRange(0, 15))
def communicate(self, command):

View File

@@ -1,159 +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:15], 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 * 32000 / scale):04X}')
if reply[:11] != f':04{self.addr:02X}000005':
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):
return self.set_par(*SETPOINT, self.scale, value)
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

@@ -3,19 +3,17 @@ from glob import glob
from pathlib import Path
from configparser import ConfigParser
from frappy.errors import ConfigError
from frappy.config import Param
class Rack:
configbase = Path('/home/l_samenv/.config/frappy_instruments')
def __init__(self, modfactory):
def __init__(self, modfactory, **kwds):
self.modfactory = modfactory
instpath = self.configbase / os.environ['Instrument']
sections = {}
self.config = {}
files = glob(str(instpath / '*.ini'))
# TODO: why may we accept several rack configs?
for filename in files:
parser = ConfigParser()
parser.optionxform = str
@@ -26,7 +24,7 @@ class Rack:
raise ConfigError(f'duplicate {section} section in {filename} and {prev}')
sections[section] = filename
self.config.update(parser.items(section))
if 'config' not in sections:
if 'rack' not in sections:
raise ConfigError(f'no rack found in {instpath}')
self.props = {} # dict (<property>, <method>) of value
self.mods = {} # dict (<property>, <method>) of list of <cfg>
@@ -41,7 +39,7 @@ class Rack:
else:
# set prop in current module
if not mod.get(prop): # do not override given and not empty property
mod[prop] = Param(value)
mod[prop] = value
def fix_props(self, method, **kwds):
for prop, value in kwds.items():
@@ -50,12 +48,12 @@ class Rack:
self.props[prop, method] = value
# set property in modules to be fixed
for mod in self.mods.get((prop, method), ()):
mod[prop] = Param(value)
mod[prop] = value
def lakeshore(self, ls_uri=None, io='ls_io', dev='ls', model='336', **kwds):
Mod = self.modfactory
self.fix_props('lakeshore', io=io, device=dev)
self.ls_model = self.config.get('ls_model', model)
self.ls_model = model
self.ls_dev = dev
ls_uri = ls_uri or self.config.get('ls_uri')
Mod(io, cls=f'frappy_psi.lakeshore.IO{self.ls_model}',
@@ -131,7 +129,7 @@ class Rack:
Mod = self.modfactory
hepump_type = hepump_type or self.config.get('hepump_type', 'no')
Mod(nv, 'frappy_psi.ccu4.NeedleValveFlow', 'flow from flow sensor or pump pressure',
flow_sensor=flow_sensor, pressure=pump_pressure, io=ccu_io, pump_type=hepump_type, **kwds)
flow_sensor=flow_sensor, pressure=pump_pressure, io=ccu_io, **kwds)
Mod(pump_pressure, 'frappy_psi.ccu4.Pressure', 'He pump pressure', io=ccu_io)
if hepump_type == 'no':
print('no pump, no flow meter - using flow from pressure alone')
@@ -139,7 +137,7 @@ class Rack:
hepump_uri = hepump_uri or self.config['hepump_uri']
Mod(hepump_io, 'frappy.io.BytesIO', 'He pump connection', uri=hepump_uri)
Mod(hepump, 'frappy_psi.hepump.HePump', 'He pump', pump_type=hepump_type,
valvemotor=hepump_mot, valve=hepump_valve)
valvemotor=hepump_mot, valve=hepump_valve, flow=nv)
Mod(hepump_mot, 'frappy_psi.hepump.Motor', 'He pump valve motor', io=hepump_io, maxcurrent=2.8)
Mod(hepump_valve, 'frappy_psi.butterflyvalve.Valve', 'He pump valve', motor=hepump_mot)
Mod(flow_sensor, 'frappy_psi.sensirion.FlowSensor', 'Flow Sensor', io=hepump_io, nsamples=160)

View File

@@ -124,7 +124,7 @@ class HeLevel(Base, Readable):
return self.command(hfu=value)
class ValveBase(Base, Writable):
class Valve(Base, Writable):
value = Parameter('relay state', BoolType())
target = Parameter('relay target', BoolType())
ioClass = IO
@@ -138,44 +138,38 @@ class ValveBase(Base, Writable):
_open_command = None
_close_command = None
_query_state = None
_set_time = 0
def write_target(self, target):
self._set_time = time.time()
if target:
self.command(**self._open_command)
else:
self.command(**self._close_command)
self.value = target
def read_status(self):
state = int(self.command(**self._query_state))
value, status = self.STATE_MAP[state]
if time.time() > self._set_time + 2:
self.value = self.target = value
self.value, status = self.STATE_MAP[state]
return status
class HeFillValve(ValveBase):
class HeFillValve(Valve):
_open_command = {'hcd': 1, 'hf': 1}
_close_command = {'hcd': 0, 'hf': 0}
_query_state = {'hv': int}
class N2FillValve(ValveBase):
class N2FillValve(Valve):
_open_command = {'nc': 1}
_close_command = {'nc': 0}
_query_state = {'nv': int}
class Valve(ValveBase):
class AuxValve(Valve):
channel = Property('valve number', IntRange(1, 12))
def initModule(self):
self._open_command = {f'vc{self.channel}': 1}
self._close_command = {f'vc{self.channel}': 0}
self._query_state = {f'v{self.channel}': int}
super().initModule()
class N2TempSensor(Readable):
@@ -806,26 +800,3 @@ class NeedleValveFlow(HasStates, Base, Drivable):
self.log.debug('before %g pulse %g, flowstep %g', sm.flow_before, sm.open_pulse, sm.last[-1] - sm.flow_before)
self.close()
return self.final_status(IDLE, '')
class MotorValve(Base, Drivable):
STATUS = {'1': (BUSY, 'opening'),
'2': (BUSY, 'closing'),
'3': (IDLE, 'opened'),
'4': (IDLE, 'closed'),
'5': (ERROR, 'no motor'),
}
value = Parameter('value', datatype=BoolType())
target = Parameter('target', datatype=BoolType(), readonly=False)
def write_target(self, target):
cmd = 1
if target == 0:
cmd = -1
self.communicate(f'mp{60 * cmd}')
def read_status(self):
status = self.communicate('fm')
return self.STATUS.get(status, (ERROR, f'undefined status: {status}'))

View File

@@ -1,192 +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>
# *****************************************************************************
""" CRYOMECH CP-2800 compressor
https://github.com/Flasew/pt415/blob/master/pt415.py """
import struct
from frappy.core import BytesIO, HasIO, Writable, Parameter, BoolType, FloatRange
from frappy.rwhandler import ReadHandler
from frappy.errors import CommunicationFailedError
class IO(BytesIO):
timeout = 5
def getFullReply(self, request, replyheader):
while replyheader[-1] != 13: # CR
replyheader += self.readBytes(1)
return replyheader
HW_ADDR = { 'running_hours': (0x454c, 0, 60),
'T_cpu': (0x3574, 0, 10),
'motor_current': (0x638b, 0, 1),
'T_inp_water': (0x0d8f, 0, 10),
'T_inp_water_min': (0x6e58, 0, 10),
'T_inp_water_max': (0x8a1c, 0, 10),
'T_out_water': (0x0d8f, 1, 10),
'T_out_water_min': (0x6e58, 1, 10),
'T_out_water_max': (0x8a1c, 1, 10),
'T_helium': (0x0d8f, 2, 10),
'T_helium_min': (0x6e58, 2, 10),
'T_helium_max': (0x8a1c, 2, 10),
'T_oil': (0x0d8f, 3, 10),
'T_oil_min': (0x6e58, 3, 10),
'T_oil_max': (0x8a1c, 3, 10),
'p_high_side': (0xaa50, 0, 10),
'p_high_side_min': (0x5e0b, 0, 10),
'p_high_side_max': (0x7a62, 0, 10),
'p_high_side_avg': (0x7e90, 0, 10),
'p_low_side': (0xaa50, 1, 10),
'p_low_side_min': (0x5e0b, 1, 10),
'p_low_side_max': (0x7a62, 1, 10),
'p_low_side_avg': (0xbb94, 0, 10),
'p_delta_avg': (0x319c, 0, 10),
'p_high_side_bounce': (0x66fa, 0, 10)
}
MIN_LEN = 14
ADDR = b'\x10'
STX = b'\x02'
CMD = b'\x80'
CR = b'\x0D'
DATA_WRITE = b'\x61'
DATA_READ = b'\x63'
ESC = b'\x07'
ESC_STX = b'\x07\x30'
ESC_CR = b'\x07\x31'
ESC_ESC = b'\x07\x32'
table = {STX: ESC_STX, CR: ESC_CR, ESC: ESC_ESC}
class CP2800(HasIO, Writable):
ioClass = IO
value = Parameter('state of compressor (on/off)', datatype=BoolType())
target = Parameter('state of compressor (on/off)', datatype=BoolType(), readonly=False)
running_hours = Parameter('running hours', datatype=FloatRange(unit='h'))
T_cpu = Parameter('CPU temperature', datatype=FloatRange(unit='degC'))
motor_current = Parameter('motor current', datatype=FloatRange(unit='A'))
T_inp_water = Parameter('inp water temperature', datatype=FloatRange(unit='degC'))
T_inp_water_min = Parameter('min inp water temperature', datatype=FloatRange(unit='degC'))
T_inp_water_max = Parameter('max inp water temperature', datatype=FloatRange(unit='degC'))
T_out_water = Parameter('out water temperature', datatype=FloatRange(unit='degC'))
T_out_water_min = Parameter('min out water temperature', datatype=FloatRange(unit='degC'))
T_out_water_max = Parameter('max out water temperature', datatype=FloatRange(unit='degC'))
T_helium = Parameter('helium temperature', datatype=FloatRange(unit='degC'))
T_helium_min = Parameter('min helium temperature', datatype=FloatRange(unit='degC'))
T_helium_max = Parameter('max helium temperature', datatype=FloatRange(unit='degC'))
T_oil = Parameter('oil temperature', datatype=FloatRange(unit='degC'))
T_oil_min = Parameter('min oil temperature', datatype=FloatRange(unit='degC'))
T_oil_max = Parameter('max oil temperature', datatype=FloatRange(unit='degC'))
p_high_side = Parameter('high side pressure', datatype=FloatRange(unit='psi'))
p_high_side_min = Parameter('min high side pressure', datatype=FloatRange(unit='psi'))
p_high_side_max = Parameter('max high side pressure', datatype=FloatRange(unit='psi'))
p_high_side_avg = Parameter('average high side pressure', datatype=FloatRange(unit='psi'))
p_low_side = Parameter('low side pressure', datatype=FloatRange(unit='psi'))
p_low_side_min = Parameter('min low side pressure', datatype=FloatRange(unit='psi'))
p_low_side_max = Parameter('max low side pressure', datatype=FloatRange(unit='psi'))
p_low_side_avg = Parameter('average low side pressure', datatype=FloatRange(unit='psi'))
p_delta_avg = Parameter('average delta pressure', datatype=FloatRange(unit='psi'))
p_high_side_bounce = Parameter('high side bounce pressure', datatype=FloatRange(unit='psi'))
def checksum(self, msg):
cksum = sum(msg)
cksum0 = ((cksum & 0xF0) >> 4) + 0x30
cksum1 = (cksum & 0x0F) + 0x30
return bytes([cksum0, cksum1])
def makePacket(self, dhash, index, val=None):
"""Make a message packet from the given parameter.
dhash {int} - hash code representing the command.
index {int} - index of the command.
val {int} - if the variable is to be written into the controller, this is the value to write.
"""
msg = ADDR + CMD
if val is None:
msgtype = DATA_READ
else:
msgtype = DATA_WRITE
msg += msgtype
payload = struct.pack('>HB', dhash, index)
if val is not None:
payload += struct.pack('>I', val)
cksum = self.checksum(msg + payload)
# replace ESC first to avoid recursive replaces
msg_payload = payload.replace(ESC, ESC_ESC) \
.replace(CR, ESC_CR) \
.replace(STX, ESC_STX)
return STX + msg + msg_payload + cksum + CR
def deflatePacket(self, msg, reading=True):
"""Decode a returned packet. Runs checksum to check if the message is
valid. If the original command were to read information from the controller
this function will return the value read assume it's valid; if the
original command were to write some value, this function will return 0
on success.
msg {str} - message to be decoded
reading {bool} - if the original message was to read some value.
"""
if len(msg) < 6:
raise CommunicationFailedError('deflatePacket: packet too short!')
begin = msg[:3]
middle = msg[3:-3]
end = msg[-3:]
middle = middle.replace(ESC_STX,STX) \
.replace(ESC_CR,CR) \
.replace(ESC_ESC,ESC)
msg = begin+middle+end
cksum = self.checksum(msg[1:-3])
if cksum[0] != msg[-3] or cksum[1] != msg[-2]:
raise CommunicationFailedError('bad checksum')
if reading:
data = struct.unpack('>I', middle[-4:])[0]
return data
else:
return 0
def write_target(self, target):
index = 0
dhash = 0xd501 if target else 0xc598
msg = self.makePacket(dhash, index, target)
reply = self.communicate(msg, 6)
answer = self.deflatePacket(reply, reading=False)
if answer != 0:
return CommunicationFailedError('error when deflating packet')
return self.read_value()
def read_value(self):
return self.read_motor_current() > 0
@ReadHandler(HW_ADDR)
def read_addressed(self, pname):
dhash, index, scale = HW_ADDR[pname]
msg = self.makePacket(dhash, index)
reply = self.communicate(msg, MIN_LEN)
data = self.deflatePacket(reply, reading=True)
return float(data) / scale
def doPoll(self):
msg = self.makePacket(0xd3db, 0) # send mm reset (from sea)
reply = self.communicate(msg, MIN_LEN)
answer = self.deflatePacket(reply, reading=False)
if answer != 0:
return CommunicationFailedError('error when deflating packet')

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': '',
}

View File

@@ -1,37 +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>
# *****************************************************************************
from frappy.core import StringIO, HasIO, Readable, Parameter, Property, FloatRange
class IO(StringIO):
end_of_line = '\r'
identification = [('IDN*', '.*')] # expected reply?
default_settings = {'baudrate': 9600}
class Pressure(HasIO, Readable):
ioClass = IO
value = Parameter('pressure', datatype=FloatRange(unit='mbar'))
scale = Property('global scale factor', datatype=FloatRange(), default=1)
def read_value(self):
return float(self.communicate('PRES?')) / self.scale # any other reply?

View File

@@ -1,92 +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>
# *****************************************************************************
"""used for EPC8210 power line switch"""
import re
import struct
from frappy.core import BytesIO, HasIO, Writable, Parameter, Property, IntRange, \
BoolType, BLOBType, Command
class IO(BytesIO):
default_settings = {'baudrate': 115200}
# pattern matches the terminal-screen-like answer, to include '\n' in .* use flag re.DOTALL
PAT = re.compile(b''.join([f'.*<{i}> (ON|OFF)'.encode() for i in range(1,9)]), re.DOTALL)
switch_status = None
@Command((BLOBType(), IntRange(0)), result=BLOBType(0,999))
def communicate(self, request, replylen):
return super().communicate(request, replylen)
def initModule(self):
self.modules = []
super().initModule()
def register(self, module):
self.modules.append(module)
def doPoll(self):
# read values
reply = self.communicate(b'\x1b\r', 360) # binary escape, take first 360 bytes of answer
match = self.PAT.match(reply)
if match:
self.switch_status = match.groups()
self.log.debug('%r', self.switch_status)
else:
# avoid recursive issue if no match is made
self.switch_status = ['OFF'] * 8
return
for module in self.modules:
module.update_values()
class Base(HasIO):
def initModule(self):
self.io.register(self)
super().initModule()
def get_value(self, addr):
return self.io.switch_status[addr - 1] == b'ON'
def update_values(self):
self.read_value()
class Switch(Base, Writable):
ioClass = IO
value = Parameter('state of power line (on/off)', datatype=BoolType())
target = Parameter('target value of power line (on/off)', datatype=BoolType(), readonly=False)
addr = Property('address of switch', datatype=IntRange(1, 8))
def read_value(self):
if self.io.switch_status is None:
self.io.doPoll()
return self.get_value(self.addr)
def write_target(self, target):
cmd = 0x32 - int(target)
command = struct.pack('BBBBBB', 0x80, cmd, self.addr, cmd ^ self.addr, 0x1b, 0xd)
reply = self.communicate(command, 360)
match = self.io.PAT.match(reply)
if match:
self.io.switch_status = match.groups()
return self.read_value()

View File

@@ -1,155 +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>
# *****************************************************************************
""" function generator agilent 33210a """
import re
from frappy.core import StringIO, HasIO, Writable, Parameter, FloatRange, EnumType, \
IDLE, WARN, nopoll
class IO(StringIO):
end_of_line = '\n'
identification = [('*IDN?', 'Agilent Technologies,33210A.*')] # Agilent Technologies,33210A,0,f.ff-b.bb-aa-p
class Frequency(HasIO, Writable):
ioClass = IO
target = Parameter('target frequency', datatype=FloatRange(unit='Hz'), readonly=False)
value = Parameter('frequency', datatype=FloatRange(unit='Hz'))
function = Parameter('function', datatype=EnumType(SIN=0, SQU=1, RAMP=2), readonly=False)
voltage = Parameter('peak-to-peak voltage', datatype=FloatRange(unit='V'), readonly=False)
offset = Parameter('voltage offset', datatype=FloatRange(unit='V'), readonly=False)
width = Parameter('width', datatype=FloatRange(unit='s'), readonly=False)
burstcycles = Parameter('burst cycles', datatype=FloatRange(), readonly=False)
burstperiod = Parameter('burst period', datatype=FloatRange(unit='s'), readonly=False)
mode = Parameter('device mode', datatype=EnumType(continuous=0, burst=1, off=2), readonly=False)
_prev_mode = ''
def parse_reply(self, reply):
match = re.match(r'"([A-Z]*) (.*),(.*),(.*)"', reply)
self.function = match.group(1)
self.value = float(match.group(2))
self.voltage = float(match.group(3))
self.offset = float(match.group(4))
def read_value(self):
reply = self.communicate('APPL?')
self.parse_reply(reply)
return self.value
@nopoll
def read_target(self):
return self.read_value()
def write_target(self, target):
cmd = f'APPL:{self.function.name} {float(target)} HZ, {float(self.voltage)} VPP, {float(self.offset)}'
reply = self.communicate(f'{cmd}\nAPPL?')
self.parse_reply(reply)
return self.value
def write_function(self, function):
self.function = function
self.write_target(self.target)
self.read_value()
def write_voltage(self, voltage):
self.voltage = voltage
self.write_target(self.target)
self.read_value()
def write_offset(self, offset):
self.offset = offset
self.write_target(self.target)
self.read_value()
def read_width(self):
return float(self.communicate('FUNC:PULS:WIDT?'))
def write_width(self, width):
self.communicate(f'FUNC:PULS:WIDT {width}')
return self.read_width()
def read_burstcycles(self):
return int(float(self.communicate('BURS:NCYC?')))
def write_burstcycles(self, burstcycles):
self.communicate(f'BURS:NCYC {int(burstcycles)}')
return self.read_burstcycles()
def read_burstperiod(self):
return float(self.communicate('BURS:INT:PER?'))
def write_burstperiod(self, burstperiod):
self.communicate(f'BURS:INT:PER {burstperiod}')
return self.read_burstperiod()
def read_mode(self):
reply = self.communicate('OUTP?')
if reply == '0':
return 'off'
reply = self.communicate('BURS:STAT?')
if reply == 'ON':
return 'burst'
return 'continuous'
def write_mode(self, mode):
if mode == 'off':
reply = self.communicate('OUTP OFF;OUTP?')
if reply == '0':
self.status = WARN, 'device is turned off'
else:
self.status = WARN, 'error when turning device off'
if mode == 'burst':
reply = self.communicate('OUTP ON;BURS:STAT ON\nBURS:STAT?')
if reply == '1':
self.status = IDLE, 'burst mode'
else:
self.status = WARN, 'error when turning burst mode on'
if mode == 'continuous':
reply = self.communicate('OUTP ON;BURS:STAT OFF\nBURS:STAT?')
if reply == '0':
self.status = IDLE, 'continuous mode'
else:
self.status = WARN, 'error when turning continuous mode on'
self._prev_mode = mode
return mode
# def read_function(self):
# return self.communicate('FUNC?')
# def write_function(self, function):
# self.communicate(f'FUNC {function}')
# return self.read_function()
# def read_voltage(self):
# txtval, unit = self.communicate('VOLT?').split('')
# return float(txtval)
# def write_voltage(self, voltage):
# self.communicate(f'VOLT {voltage:.1f} VPP')
# return self.read_voltage()
# def read_offset(self):
# return float(self.communicate('VOLT:OFFS?'))
# def write_offset(self, offset):
# self.communicate(f'VOLT:OFFS {offset:.1f}')
# return self.read_offset()

View File

@@ -41,17 +41,15 @@ class HePump(Writable):
self.valvemotor.write_output0(value)
def read_target(self):
if self.valvemotor.io.is_connected:
return self.valvemotor.read_output0()
return self.target
return self.valvemotor.read_output0()
def read_value(self):
if self.has_feedback and self.valvemotor.io.is_connected:
if self.has_feedback:
return not self.valvemotor.read_input3()
return self.target
def read_eco_mode(self):
if self.pump_type == 'xds35' and self.valvemotor.io.is_connected:
if self.pump_type == 'xds35':
return self.valvemotor.read_output1()
return False

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

@@ -1,150 +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>
# *****************************************************************************
from frappy.core import Parameter, Property, Writable, Attached, EnumType, FloatRange, \
IDLE, BUSY, WARN
STATES = {'manual': 0,
'high_pressure': 1,
'circulating': 2,
'warmup': 3,
}
class JTCCR(Writable):
compressor = Attached()
value = Parameter('current state', datatype=EnumType(STATES), default=0)
target = Parameter('target state', datatype=EnumType(STATES), default=0)
#p1min = Property('lower limit to switch to high pressure mode', dataype=FloatRange(unit='mbar'), default=1.8)
p1max = Parameter('limit to switch to circulating mode', datatype=FloatRange(unit='mbar'),
readonly=False, default=2.2)
p2min = Parameter('lower limit to turn compressor off', datatype=FloatRange(unit='mbar'),
readonly=False, default=0.12)
p2max = Parameter('upper limit to turn compressor on', datatype=FloatRange(unit='mbar'),
readonly=False, default=0.8)
#p2lim = Property('do not start compressor if p2 is below this value', datatype=FloatRange(unit='mbar'), default=0.15)
pdifmax = Parameter('max pressure difference of compressor', datatype=FloatRange(unit='mbar'),
readonly=False, default=5.0)
pdifmargin = Parameter('safety margin for pressure difference of compressor',
datatype=FloatRange(unit='mbar'),
readonly=False, default=1.0)
p3margin = Parameter('start compressor when p3 is below pressreg setpoint plus this value',
datatype=FloatRange(unit='mbar'),
readonly=False, default=0.01)
p3reg = Parameter('pressure regulation setpoint', datatype=FloatRange(unit='mbar'), default=4.0)
plow = Parameter('pressure below 5K', datatype=FloatRange(unit='mbar'),
readonly=False, default=4.0)
valves_high_pressure = {
'close': 'v3 v4 v5 v6 v7 v8 v10',
'open': 'v1 v2 v9', # vm
}
valves_circulating = {
'close': 'v3 v4 v5 v6 v7 v9 v10',
'open': 'v1 v2 v8', # vm
}
valves_warmup = {
'close': 'v6 v7 v8 v9 v10',
'open': 'v1 v2 v3 v4 v5', # vm
}
valves_security= {
'open': '',
'close': 'v1 v9'
}
valves_overpressure = {
'open': 'v10',
'close': ''
}
def write_target(self, target):
if self.value != target:
self.log.info('set mode %r', target)
self.set_mode(target)
return target
def set_mode(self, state):
if state == 'high_pressure':
self.p3reg = 12
self.handle_valves(**self.valves_high_pressure)
elif state == 'circulating':
self.p3reg = self.plow
self.handle_valves(**self.valves_circulating)
elif state == 'warmup':
self.handle_valves(**self.valves_warmup)
elif state != 'manual':
self.log.error('unknown state %r', state)
self.value = state
def security_settings(self):
self.compressor.write_target(False)
self.handle_valves(**self.valves_security)
self.set_mode('manual')
def handle_valves(self, close=(), open=()):
"""set given valves. raises ImpossibleError, when checks fails"""
self.log.info('handle_valves %r %r', close, open)
self._valves_to_wait_for = {}
self._valves_failed = {True: [], False: []}
for flag, valves in enumerate([close, open]):
for vname in valves.split():
valve = self.secNode.modules[vname]
self.log.info('set valve %s to %r', vname, flag)
valve.write_target(flag)
# TODO: do we need to wait for motor valve?
def doPoll(self):
p1 = self.secNode.modules['p1'].read_value()
p2 = self.secNode.modules['p2'].read_value()
p3 = self.secNode.modules['p3'].read_value()
compressor_state = self.compressor.read_value()
if self.value == 'manual':
return self.set_mode('manual')
if p3 >= p2 + self.pdifmax + self.pdifmargin:
# overpressure protection
self.security_settings()
self.status = WARN, 'overpressure: He recovery output closed?'
return
if p2 < self.p2min and p3 < self.p3reg and self.value != 'high_pressure':
# underpressure protection
self.security_settings()
self.status = WARN, 'underpressure: not enough He'
return
if self.value == 'circulating':
if p3 < self.p3reg and not compressor_state:
self.compressor.write_target(True)
elif (p3 - p2) > self.pdifmax and compressor_state:
self.compressor.write_target(False)
# TODO: do we need to skip overpressure protection for one time?
if self.value == 'high_pressure' and p1 > self.p1max:
self.set_mode('circulating')
self.status = IDLE, ''
if p2 > self.p2max and not compressor_state:
self.compressor.write_target(True)
elif p2 < self.p2min and compressor_state:
self.compressor.write_target(False)
if (p3 - p2) >= self.pdifmax + 0.1:
self.handle_valves(**self.valves_overpressure)
self.status = WARN, 'release to recovery'
elif self.secNode.modules['v10'].read_value():
self.secNode.modules['v10'].write_target(False)
self.status = IDLE, 'release finished'
self.status = IDLE, ''

View File

@@ -50,7 +50,6 @@ SOURCECMDS = {
class SourceMeter(HasIO, Module):
ioClass = K2601bIO
export = False # export for tests only
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
readonly=False, export=False)
@@ -108,7 +107,6 @@ class Resistivity(HasIO, Readable):
class Current(HasIO, Writable):
ioClass = K2601bIO
sourcemeter = Attached()
value = Parameter('measured current', FloatRange(unit='A'))
@@ -155,7 +153,6 @@ class Current(HasIO, Writable):
class Voltage(HasIO, Writable):
ioClass = K2601bIO
sourcemeter = Attached()
value = Parameter('measured voltage', FloatRange(unit='V'))

View File

@@ -16,8 +16,9 @@
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""base classes for various lakeshore temperature monitors/controllers"""
"""driver for various lakeshore temperature monitors/controllers"""
import time
import math
import random
import threading
@@ -32,10 +33,10 @@ from frappy.errors import CommunicationFailedError, ConfigError, \
HardwareError, DisabledError, ImpossibleError, secop_error, SECoPError
from frappy.lib.units import NumberWithUnit, format_with_unit
from frappy.lib import formatStatusBits
from frappy_psi.calcurve import CalCurve
from frappy_psi.convergence import HasConvergence
from frappy.mixins import HasOutputModule, HasControlledBy
from frappy.extparams import StructParam
from frappy_psi.calcurve import CalCurve
def string_to_num(string):
@@ -129,11 +130,13 @@ class Device(HasLscIO, Module):
log_formats = True, False # tuple (<log Ohm allowed>, <log K allowed>)
max_curve_length = 200
cmds_per_line = 5 # number of commands / replies allowed on one line
_empty_curves = None
_request_lock = None
FAST_POLL = 0.01
def initModule(self):
self._to_delete = [] # curves to delete
self._empty_curves = {} # dict <curve no> of None (used as ordered set)
self._curve_map = {} # dict CurveHeader.key of curve_no
self._requests = {} # dict <calcurve> of CurveRequest
self._sensors = {} # dict <channel> of sensors
self._disable_channels = set(self.channels)
@@ -186,6 +189,12 @@ class Device(HasLscIO, Module):
for curve_no in range(*self.user_curves):
crvhdr = CurveHeader(*headers[curve_no].split(','))
self.curve_cache[curve_no] = crvhdr
if crvhdr.key:
if crvhdr in self._curve_map:
# this is a duplicate, add to empty curves
self._empty_curves[curve_no] = None
else:
self._curve_map[crvhdr.key] = curve_no
try:
check_item = None
with self._request_lock:
@@ -211,10 +220,6 @@ class Device(HasLscIO, Module):
sensor = self._sensors.get(ch)
if sensor is None or sensor.disabled:
self.disable_channel(ch)
if self._to_delete:
no = self._to_delete.pop(0)
if self.curve_cache[no].key is None:
self.command(f'CRVDEL {no}')
except Exception as e:
request.loading = False
request.status = ERROR, repr(e)
@@ -233,23 +238,25 @@ class Device(HasLscIO, Module):
def get_empty(self):
"""get curve no of a curve to be reused"""
for curve_no in range(*self.user_curves):
if not self.curve_cache[curve_no].key:
return curve_no
used_no = set(s.curve_no for s in self._sensors.values())
for req in self._requests.values():
used_no.add(req.invalidate)
n0, n = self.user_curves
n -= n0
# instead of taking the lowest available number, start at a
# random index, else most recent curves would be overridden
offset = random.randrange(n)
for i in range(n):
curve_no = n0 + (i + offset) % n
if curve_no not in used_no:
break
if self._empty_curves:
# we have unused curve slots
curve_no = next(iter(self._empty_curves))
self._empty_curves.pop(curve_no)
else:
raise ValueError('no empty curves available')
used_no = set(s.curve_no for s in self._sensors.values())
for req in self._requests.values():
used_no.add(req.invalidate)
n0, n = self.user_curves
n -= n0
# avoid to take the lower numbers first
# as then the most recent curves would be overridden
offset = random.randrange(n)
for i in range(n):
curve_no = n0 + (i + offset) % n
if curve_no not in used_no:
break
else:
raise ValueError('no empty curves available')
return curve_no
def get_calib_state(self, sensor):
@@ -263,7 +270,7 @@ class Device(HasLscIO, Module):
return ImpossibleError('error while loading calibration curve')
return None
def verify_ends(self, request, no):
def verify_ends(self, request):
"""preliminary check: check if the ends of the curve are matching the stored ones"""
npnt = len(request.points)
numbers = [1, npnt]
@@ -271,12 +278,8 @@ class Device(HasLscIO, Module):
if npnt < self.max_curve_length:
numbers.append(npnt + 1)
points.append((0, 0))
stored_points = self.get_crvpts(no, *numbers)
for pairs in zip(points, stored_points):
for pairs in zip(points, self.get_crvpts(request.curve_no, *numbers)):
if not self.is_equal(*pairs):
self.log.info('not equal %r', pairs)
self.log.info('requested points %r', points)
self.log.info('stored points %r', stored_points)
return False
return True
@@ -286,25 +289,11 @@ class Device(HasLscIO, Module):
:param request: the curve request
:return: next action
"""
key = request.crvhdr.key
duplicates = []
for no, crvhdr in self.curve_cache.items():
if key == crvhdr.key:
if self.verify_ends(request, no):
ends_ok = True
for delno in duplicates:
self.curve_cache[delno] = CurveHeader()
self._to_delete.append(delno)
break
else:
duplicates.append(no)
else:
no = None
ends_ok = False
if no is not None:
no = self._curve_map.get(request.crvhdr.key)
if no:
request.crvhdr = self.curve_cache[no]
request.set_curve_no(no)
if ends_ok:
if self.verify_ends(request):
self.log.info('calcurve #%d %s found, start to check consistency', no, request.crvhdr)
# guess the curve is o.k. -> install
request.install_sensors(request.sensors.values())
@@ -317,8 +306,8 @@ class Device(HasLscIO, Module):
request.invalidate = no
request.set_curve_no(self.get_empty())
request.install_sensors(request.sensors.values())
self.log.info('%s found as #%d, but content has changed, create #%d',
request.crvhdr.sn, no, request.curve_no)
self.log.info('%s found, but content has changed, create #%d',
request.crvhdr.sn, request.curve_no)
return self.start_load
request.set_curve_no(self.get_empty())
for sensor in request.sensors.values():
@@ -402,6 +391,7 @@ class Device(HasLscIO, Module):
:return: next action
"""
self.curve_cache[request.curve_no] = CurveHeader(*request.crvhdr)
self._curve_map[request.crvhdr.key] = request.curve_no
self.put_header(request.curve_no, *request.crvhdr)
request.install_sensors(request.sensors.values())
no = request.invalidate
@@ -410,7 +400,7 @@ class Device(HasLscIO, Module):
self.log.info('calcurve #%d %s %s, clear previous #%d',
request.curve_no, request.crvhdr, comment, no)
self.curve_cache[no] = CurveHeader()
self._to_delete.append(no)
self._empty_curves[no] = None
else:
self.log.info('calcurve #%d %s %s',
request.curve_no, request.crvhdr, comment)
@@ -451,7 +441,7 @@ class Device(HasLscIO, Module):
"""get all headers
for performance reasons, by default 5 headers are read in one line
this speeds up quite a lot on serial connections
this is speeing up quite a lot on serial connections
"""
n0, n1 = self.user_curves
result = {}
@@ -476,7 +466,7 @@ class CurveRequest:
self.sensors = {sensor.channel: sensor}
calcurve = CalCurve(sensor.calcurve)
equipment_id = device.propertyValues.get('original_id') or device.secNode.equipment_id
self.name = f"{equipment_id.split('.')[0]}.{sensor.name}"
name = f"{equipment_id.split('.')[0]}.{sensor.name}"
sn = calcurve.calibname
limit = calcurve.calibrange[1]
unit = calcurve.options.get('unit', 'Ohm')
@@ -506,11 +496,9 @@ class CurveRequest:
else:
fmt = 2 if unit == 'V' else 1
coef = 1 + (self.points[0][1] < self.points[-1][1]) # 1: ntc, 2: ptc
self.crvhdr = CurveHeader(self.name, sn, fmt, limit, coef)
self.crvhdr = CurveHeader(name, sn, fmt, limit, coef)
def set_curve_no(self, curve_no):
if self.name != self.crvhdr.name:
self.crvhdr = CurveHeader(self.name, *self.crvhdr[1:])
self.curve_no = curve_no
for s in self.sensors.values():
s.curve_no = curve_no
@@ -624,19 +612,16 @@ class Sensor(Base, Readable):
@nopoll
def read_value(self):
self.status, value, raw = self.get_data()
self.status, value, self.raw = self.get_data()
if isinstance(value, SECoPError):
raise value
if not isinstance(raw, SECoPError):
self.raw = raw
raise value.copy()
return value
@nopoll
def read_raw(self):
self.status, value, raw = self.get_data()
self.status, self.value, raw = self.get_data()
if isinstance(raw, SECoPError):
raise raw
self.value = value
raise raw.copy()
return raw
def read_status(self):
@@ -1004,9 +989,265 @@ class Loop(HasConvergence, HasOutputModule, Sensor):
return self.query(f'SETP?{self.output_module.output_no}', float)
# --- MODEL 340 ---
class IO340(IO):
timeout = 5 # needed for INCRV command
model = 340
end_of_line = '\r' # default at SINQ. TODO: remove
class Device340(Device):
ioClass = IO340
model = 340
channels = 'ABCD'
user_curves = (21, 61) # the last curve is 60
log_formats = True, True # log Ohm / log K is supported
_crvsav_deadline = None
def disable_channel(self, channel):
self.communicate(f'INSET {channel},0;*OPC?')
def finish_curve(self, request):
super().finish_curve(request)
if request.loading and not self._crvsav_deadline:
# when loading a new curve, remember for sending CRVSAV later
self._crvsav_deadline = time.time() + 600
def put_header(self, curve_no, name, sn, fmt, limit, coef):
self.communicate(f'CRVHDR {curve_no},{name:15s},{sn:10s},{fmt},{limit},{coef};*OPC?')
def doPoll(self):
super().doPoll()
if self._crvsav_deadline:
# prevent flashing multiple times within short time
if time.time() > self._crvsav_deadline:
self._crvsav_deadline = None
self.communicate('CRVSAV;*OPC?')
def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-4), significant=6):
# for whatever reason, the number of digits after decimal point for the T column is only 4
return super().is_equal(left, right, fixeps, significant)
class Sensor340(Sensor):
model = 340
intype = None # arguments for the intype command
def get_curve_type(self, calcurve):
unit = calcurve.options.get('unit', 'Ohm')
logformat = False
range_limit = None
if unit == 'Ohm':
if calcurve.ptc:
xlim = calcurve.xrange[1] * 0.001 # 1 mA excitation
for rng, limit in enumerate(
[0, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05,
0.1, 0.25, 0.5, 1, 2.5, 5]):
if xlim <= limit:
break
else:
rng = 13
# we use special type here, in order to allow thermal compensation
# <type>, <units>, <coefficient>, <excitation>, <range>
self.intype = 0, 2, 2, 10, rng
else:
if calcurve.options.get('type') == 'GE':
self.intype = (10,)
else:
self.intype = (8,) # carbon and ruox are equivalent to cernox(8)
logformat = True, True
elif unit == 'V': # diode
self.intype = (1,) if calcurve.xscale[1] < 2.5 else (2,)
else: # thermocouple
self.intype = (12,)
return logformat, range_limit
def install_sensor(self):
super().install_sensor()
if self.query(f'INSET?{self.channel}', int, int) != (1, 1):
self.command(f'INSET {self.channel}', 1, 1)
class Loop340(Loop, Sensor340):
pass
class MainOutput340(MainOutput):
model = 340
output_no = Parameter(datatype=IntRange(1, 1))
HTRST_MAP = {
0: (IDLE, ''),
1: (ERROR, 'Power supply over voltage'),
2: (ERROR, 'Power supply under voltage'),
3: (ERROR, 'Output digital-to-analog Converter error'),
4: (ERROR, 'Current limit digital-to-analog converter error'),
5: (ERROR, 'Open heater load'),
6: (ERROR, 'Heater load less than 10 ohms')
}
_manual_output = 0.0 # TODO: check how to set this
vmax = 50
imax = 2
max_currents = {4 - i: 2 ** (1 - i) for i in range(4)}
SETPOINTLIMS = 1500.0
sorted_factors = sorted([(fhtr * fcur ** 2, (i, h))
for i, fcur in max_currents.items()
for h, fhtr in MainOutput.heater_ranges.items()
])
def get_status(self):
st = self.query(f'HTRST?', int)
return self.HTRST_MAP[st]
def configure(self):
icurrent, htr_range = self.get_best_power_idx(self._desired_max_power, 1.1)
self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
self.command(f'CLIMIT {self.output_no}', self.SETPOINTLIMS, 0.0, 0.0, icurrent, htr_range)
self._htr_range = htr_range
if self._control_loop is None:
mode = self.query(f'CMODE?{self.output_no}', int)
if mode != 3: # open loop
self.command(f'CSET {self.output_no}', '0', 1, 0, 0) # control off
self.command(f'CMODE {self.output_no}', 3) # open loop
self.command('RANGE', self._htr_range)
self.put_manual_power(self._manual_output)
else:
self.command(f'CSET {self.output_no}', self._control_loop.channel, 1, 1, 0) # control on
self.command(f'CMODE {self.output_no}', 1) # pid
self.command('RANGE', self._htr_range)
self.put_manual_power(self._control_loop.power_offset)
self.command(f'CDISP {self.output_no}', 1, self.resistance, 1, 0)
def fix_heater_range(self):
# switch heater range on, if needed
irng = self.query('RANGE?', int)
if irng != self._htr_range:
if irng:
self.log.info('output range was changed manually')
self._htr_range = irng
else:
self.log.info('output was off - switch on again')
self.command('RANGE', self._htr_range)
def read_max_power(self):
icurrent, htr_range = self.query(f'CLIMIT? {self.output_no}', float, float, float, int, int)[3:]
htr_range = self.query('RANGE?', int) or htr_range
self._htr_range = htr_range
self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
return self.calc_power(100)
def read_htr(self):
return self.query('HTR?', float)
class AnalogOutput340(AnalogOutput):
model = 340
output_no = Parameter(datatype=IntRange(2, 2), default=2)
def configure(self):
if self._control_loop is not None:
self.put_manual_power(self.target)
else:
self.command('ANALOG 2', 0, 3, self._control_loop.channel)
self.put_manual_power(self._control_loop.power_offset)
def put_manual_power(self, value):
if self._control_loop is None:
self.command('ANALOG 2', 0, 2, 0, 0, 0, 0, 0, self.calc_percent(value))
else:
self.command(f'MOUT {self.output_no}', self.calc_percent(value))
# --- MODELS 336, 350, 224, ...
class Device2(Device):
"""second generation LakeShore models 336, 350, 224 ..."""
"""second generation LakeShore models"""
channels = 'ABCD'
user_curves = (21, 60) # the last curve is 59
max_raw_unit = 99999
TYPES = {'DT': 1, 'TG': 1, 'PT': 2, 'RF': 2, 'CX': 3, 'RX': 3, 'CC': 3, 'GE': 3, 'TC': 4}
# --- MODEL 336 ---
class IO336(IO):
model = 336
class Device336(Device2):
model = 336
class Sensor336(Sensor):
model = 336
class Loop336(Loop, Sensor336):
pass
class MainOutput336(MainOutput):
model = 336
output_no = Parameter(datatype=IntRange(1, 1))
imax = 2
vmax = 50
# 3 ranges only
heater_ranges = {3 - i: 10 ** -i for i in range(3)}
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
class SecondaryOutput336(MainOutput336):
model = 336
output_no = Parameter(datatype=IntRange(2, 2))
imax = 1.414
vmax = 35.4
max_power = Parameter(datatype=FloatRange(0, 50, unit='W'))
class AnalogOutput336(AnalogOutput):
model = 336
output_no = Parameter(datatype=IntRange(3, 4))
# --- MODEL 350 ---
class Device350(Device2):
model = 350
class Sensor350(Sensor):
model = 350
def get_curve_type(self, calcurve):
logformat, range_limit = super().get_curve_type(calcurve)
excit = 0
if self.intype[0] == 3 and calcurve.calibrange[0] > 0.2:
excit = 1 # TODO: add extra parameter for excitation
self.intype += (excit,)
return logformat, range_limit
class Loop350(Loop, Sensor350):
pass
class MainOutput350(Output):
model = 350
output_no = Parameter(datatype=IntRange(1, 1))
imax = 1.732
max_power = Parameter(datatype=FloatRange(0, 75, unit='W'))
class AnalogOutput350(AnalogOutput):
model = 350
output_no = Parameter(datatype=IntRange(3,4))
# --- MODEL 224 ---
class Device224(Device2):
model = 224
channels = 'A', 'B', 'C1', 'C2', 'C3', 'C4', 'C5', 'D1', 'D2', 'D3', 'D4', 'D5'
class Sensor224(Sensor):
model = 224

View File

@@ -1,33 +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
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""LakeShore Model 224"""
import frappy_psi.lakeshore as ls
class IO(ls.IO):
model = 224
class Device(ls.Device2):
model = 224
channels = 'A', 'B', 'C1', 'C2', 'C3', 'C4', 'C5', 'D1', 'D2', 'D3', 'D4', 'D5'
class Sensor(ls.Sensor):
model = 224

View File

@@ -1,61 +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
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""LakeShore Model 336"""
from frappy.core import Parameter, IntRange, FloatRange
import frappy_psi.lakeshore as ls
class IO(ls.IO):
model = 336
class Device(ls.Device2):
model = 336
class Sensor(ls.Sensor):
model = 336
class Loop(ls.Loop, Sensor):
pass
class MainOutput(ls.MainOutput):
model = 336
output_no = Parameter(datatype=IntRange(1, 1))
imax = 2
vmax = 50
# 3 ranges only
heater_ranges = {3 - i: 10 ** -i for i in range(3)}
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
class SecondaryOutput336(MainOutput):
model = 336
output_no = Parameter(datatype=IntRange(2, 2))
imax = 1.414
vmax = 35.4
max_power = Parameter(datatype=FloatRange(0, 50, unit='W'))
class AnalogOutput(ls.AnalogOutput):
model = 336
output_no = Parameter(datatype=IntRange(3, 4))

View File

@@ -1,194 +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
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""LakeShore Model 340"""
import time
from frappy.core import Parameter, IDLE, ERROR, IntRange
import frappy_psi.lakeshore as ls
class IO(ls.IO):
timeout = 5 # needed for INCRV command
model = 340
end_of_line = '\r' # default at SINQ. TODO: remove
class Device(ls.Device):
ioClass = IO
model = 340
channels = 'ABCD'
user_curves = (21, 61) # the last curve is 60
log_formats = True, True # log Ohm / log K is supported
_crvsav_deadline = None
def disable_channel(self, channel):
self.communicate(f'INSET {channel},0;*OPC?')
def finish_curve(self, request):
super().finish_curve(request)
if request.loading and not self._crvsav_deadline:
# when loading a new curve, remember for sending CRVSAV later
self._crvsav_deadline = time.time() + 600
def put_header(self, curve_no, name, sn, fmt, limit, coef):
self.communicate(f'CRVHDR {curve_no},{name:15s},{sn:10s},{fmt},{limit},{coef};*OPC?')
def doPoll(self):
super().doPoll()
if self._crvsav_deadline:
# prevent flashing multiple times within short time
if time.time() > self._crvsav_deadline:
self._crvsav_deadline = None
self.communicate('CRVSAV;*OPC?')
def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-4), significant=6):
# for whatever reason, the number of digits after decimal point for the T column is only 4
return super().is_equal(left, right, fixeps, significant)
class Sensor340(ls.Sensor):
model = 340
intype = None # arguments for the intype command
def get_curve_type(self, calcurve):
unit = calcurve.options.get('unit', 'Ohm')
logformat = False
range_limit = None
if unit == 'Ohm':
if calcurve.ptc:
xlim = calcurve.xrange[1] * 0.001 # 1 mA excitation
for rng, limit in enumerate(
[0, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05,
0.1, 0.25, 0.5, 1, 2.5, 5]):
if xlim <= limit:
break
else:
rng = 13
# we use special type here, in order to allow thermal compensation
# <type>, <units>, <coefficient>, <excitation>, <range>
self.intype = 0, 2, 2, 10, rng
else:
if calcurve.options.get('type') == 'GE':
self.intype = (10,)
else:
self.intype = (8,) # carbon and ruox are equivalent to cernox(8)
logformat = True, True
elif unit == 'V': # diode
self.intype = (1,) if calcurve.xscale[1] < 2.5 else (2,)
else: # thermocouple
self.intype = (12,)
return logformat, range_limit
def install_sensor(self):
super().install_sensor()
if self.query(f'INSET?{self.channel}', int, int) != (1, 1):
self.command(f'INSET {self.channel}', 1, 1)
class Loop340(ls.Loop, Sensor340):
pass
class MainOutput340(ls.MainOutput):
ioClass = IO
model = 340
output_no = Parameter(datatype=IntRange(1, 1))
HTRST_MAP = {
0: (IDLE, ''),
1: (ERROR, 'Power supply over voltage'),
2: (ERROR, 'Power supply under voltage'),
3: (ERROR, 'Output digital-to-analog Converter error'),
4: (ERROR, 'Current limit digital-to-analog converter error'),
5: (ERROR, 'Open heater load'),
6: (ERROR, 'Heater load less than 10 ohms')
}
_manual_output = 0.0 # TODO: check how to set this
vmax = 50
imax = 2
max_currents = {4 - i: 2 ** (1 - i) for i in range(4)}
SETPOINTLIMS = 1500.0
sorted_factors = sorted([(fhtr * fcur ** 2, (i, h))
for i, fcur in max_currents.items()
for h, fhtr in MainOutput.heater_ranges.items()
])
def get_status(self):
st = self.query(f'HTRST?', int)
return self.HTRST_MAP[st]
def configure(self):
if self._desired_max_power is None:
self.log.info(f'max_heater {self.writeDict} {self.max_heater}')
self.write_max_heater(self.max_heater)
icurrent, htr_range = self.get_best_power_idx(self._desired_max_power, 1.1)
self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
self.command(f'CLIMIT {self.output_no}', self.SETPOINTLIMS, 0.0, 0.0, icurrent, htr_range)
self._htr_range = htr_range
if self._control_loop is None:
mode = self.query(f'CMODE?{self.output_no}', int)
if mode != 3: # open loop
self.command(f'CSET {self.output_no}', '0', 1, 0, 0) # control off
self.command(f'CMODE {self.output_no}', 3) # open loop
self.command('RANGE', self._htr_range)
self.put_manual_power(self._manual_output)
else:
self.command(f'CSET {self.output_no}', self._control_loop.channel, 1, 1, 0) # control on
self.command(f'CMODE {self.output_no}', 1) # pid
self.command('RANGE', self._htr_range)
self.put_manual_power(self._control_loop.power_offset)
self.command(f'CDISP {self.output_no}', 1, self.resistance, 1, 0)
def fix_heater_range(self):
# switch heater range on, if needed
irng = self.query('RANGE?', int)
if irng != self._htr_range:
if irng:
self.log.info('output range was changed manually')
self._htr_range = irng
else:
self.log.info('output was off - switch on again')
self.command('RANGE', self._htr_range)
def read_max_power(self):
icurrent, htr_range = self.query(f'CLIMIT? {self.output_no}', float, float, float, int, int)[3:]
htr_range = self.query('RANGE?', int) or htr_range
self._htr_range = htr_range
self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
return self.calc_power(100)
def read_htr(self):
return self.query('HTR?', float)
class AnalogOutput340(ls.AnalogOutput):
model = 340
output_no = Parameter(datatype=IntRange(2, 2), default=2)
def configure(self):
if self._control_loop is not None:
self.put_manual_power(self.target)
else:
self.command('ANALOG 2', 0, 3, self._control_loop.channel)
self.put_manual_power(self._control_loop.power_offset)
def put_manual_power(self, value):
if self._control_loop is None:
self.command('ANALOG 2', 0, 2, 0, 0, 0, 0, 0, self.calc_percent(value))
else:
self.command(f'MOUT {self.output_no}', self.calc_percent(value))

View File

@@ -1,57 +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
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""LakeShore Model 350"""
from frappy.core import Parameter, IntRange, FloatRange
import frappy_psi.lakeshore as ls
class IO(ls.IO):
model = 350
class Device(ls.Device2):
model = 350
class Sensor(ls.Sensor):
model = 350
def get_curve_type(self, calcurve):
logformat, range_limit = super().get_curve_type(calcurve)
excit = 0
if self.intype[0] == 3 and calcurve.calibrange[0] > 0.2:
excit = 1 # TODO: add extra parameter for excitation
self.intype += (excit,)
return logformat, range_limit
class Loop(ls.Loop, Sensor):
pass
class MainOutput(ls.Output):
model = 350
output_no = Parameter(datatype=IntRange(1, 1))
imax = 1.732
max_power = Parameter(datatype=FloatRange(0, 75, unit='W'))
class AnalogOutput(ls.AnalogOutput):
model = 350
output_no = Parameter(datatype=IntRange(3,4))

View File

@@ -1,232 +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>
# *****************************************************************************
"""dilution refrigerator: Leiden Cryogenics GHS-2T-1T-700
! writing limits does not work (reading does)
! communication with turbo pumps is not implemented
"""
from frappy.core import StringIO, HasIO, Readable, Writable, Parameter, Property, EnumType, BoolType, FloatRange
from frappy.errors import CommunicationFailedError
P_SCALE = 2**14 / 1e4
class IO(StringIO):
end_of_line = '\n'
identification = [('ID?', '0\\tGHS-2T-1T-700-CF.*')]
default_settings = {'baudrate': 9600}
_key_status = None
_pressure = None
_pressure_limits = None
def initModule(self):
self.modules = []
super().initModule()
def doPoll(self):
# read key (valve) states
reply = self.communicate('KEYS?')
status, key_status = reply.split('\t')
self._key_status = key_status.split(',')
if status != '0':
raise CommunicationFailedError(f'bad reply: {reply}')
# read pressure values
reply = self.communicate('ADC?')
status, pressure = reply.split('\t')
if status != '0':
raise CommunicationFailedError(f'bad reply: {reply}')
self._pressure = pressure.split(',')
# read pressure limits
status, reply = self.communicate('SETTINGS?').split('\t')
if status != '0':
raise CommunicationFailedError(f'bad reply: {reply}')
self._pressure_limits = reply.split(',')
# read value for all registred modules
for module in self.modules:
module.update_values()
def register(self, module):
self.modules.append(module)
class Base(HasIO):
def initModule(self):
self.io.register(self)
super().initModule()
def change(self, cmd, key):
reply = self.communicate(f'{cmd} {key}')
if not reply.startswith('0'):
raise CommunicationFailedError(f'bad reply: {reply}')
def get_key(self, key):
return self.io._key_status[key - 1] == '2'
def get_pressure(self, addr):
return float(self.io._pressure[addr]) / P_SCALE
def update_values(self):
self.read_value()
KEY_MAP = {'mix': 1, # mixture compressor
'bypass': 2,
'15': 3,
'16': 4,
'gate18': 6,
'4': 7,
'5': 9,
'reset': 10,
'9': 12,
'14': 13,
'13': 15,
'ledtest': 16,
'12': 18,
'10': 19,
'11': 21,
'A0': 22,
'7': 24,
'S3': 25,
'6': 27,
'17': 28,
'8': 30,
'1': 31,
'S2': 33,
'3': 34,
'2': 36,
'0': 37,
'S1': 39,
'A9': 40,
'A8': 42,
'A2': 43,
'S5': 45,
'A10': 46,
'start': 48,
'A5': 49,
'A7': 51,
'auto': 54,
'A4': 55,
'A6': 57,
'S4': 58,
'A3': 60,
'He3': 61, # condense He3
'He4': 62, # condense He4
'circulation': 63, # normal circulation
'recovery': 64,
}
class Valve(Base, Writable):
ioClass = IO
target = Parameter('target state of valve', datatype=BoolType(), readonly=False)
value = Parameter('status of valve (open/close)', datatype=BoolType())
key = Property('key (button) number', datatype=EnumType(KEY_MAP))
def write_target(self, target):
"""toggle button state (as if pressed manually on the panel)"""
self.io.doPoll()
state = self.read_value()
if state != target:
self.change('DEVMAN', self.key.value)
self.io.doPoll()
self.read_value()
def read_value(self):
if self.io._key_status is None:
self.io.doPoll()
return self.get_key(self.key.value)
PRESSURE_MAP = {'P1': 0,
'P2': 1,
'P3': 2,
'P4': 3,
'P5': 4,
'P6': 5,
'P7': 6,
'flow': 7,
}
class Pressure(Base, Readable):
ioClass = IO
value = Parameter('pressure and flow values', datatype=FloatRange(unit='mbar'))
addr = Property('address of pressure sensor', datatype=EnumType(PRESSURE_MAP))
def read_value(self):
value = self.get_pressure(self.addr.value)
return value
class PressureLimit(Pressure, Readable):
"""P6 and P7 pressure sensors of dilution refrigerator (Leiden Cryogenics GHS-2T-1T-700)
have pressure limit settings"""
ioClass = IO
value = Parameter('pressure values for sensors with limit', datatype=FloatRange(unit='mbar'))
offset = Property('address of pressure sensor', datatype=EnumType({'P6': 0, 'P7': 2}))
limit_low = Parameter('low pressure limit', datatype=FloatRange(0, 2000, unit='mbar'), readonly=False)
limit_high = Parameter('high pressure limit', datatype=FloatRange(0, 2000, unit='mbar'), readonly=False)
def get_pressure_limit(self, lim):
return float(self.io._pressure_limits[lim])
def set_limit(self, pos, limit):
limit_list = self.io._pressure_limits
limit_list[pos] = f'{limit:.0f}'
reply = self.communicate(f'SETTINGS {",".join(limit_list)},0')
if reply != '0':
raise CommunicationFailedError(f'bad reply: {reply}')
self.io.doPoll()
def read_limit_low(self):
return self.get_pressure_limit(self.offset.value)
def read_limit_high(self):
return self.get_pressure_limit(self.offset.value + 1)
#self._P6_low, self._P6_high, self._P7_low, self._P7_high = reply.split(',')
def write_limit_low(self, limit):
return self.set_limit(self.offset.value, limit)
def write_limit_high(self, limit):
return self.set_limit(self.offset.value + 1, limit)
def update_values(self):
self.read_value()
self.read_limit_low()
self.read_limit_high()
class Turbo(Base, Writable):
ioClass = IO
target = Parameter('target state of turbo pump', datatype=EnumType(off=0, on=1))
value = Parameter('state of turbo pump', datatype=EnumType(off=0, on=1))
addr = Parameter('turbo pump address', datatype=EnumType(turbo1=1, turbo2=2))
# def write_target(self, target):
# self.change(f'TURBO ON/OFF {target}', addr)

View File

@@ -21,8 +21,9 @@ import sys
from time import monotonic
from ast import literal_eval
import snap7
from frappy.core import Attached, Command, Readable, Parameter, FloatRange, HasIO, Property, StringType, \
IDLE, BUSY, WARN, ERROR, Writable, Drivable, BoolType, IntRange, Communicator, StatusType
from frappy.core import Attached, Command, Readable, Parameter, FloatRange, HasIO, Property, \
IDLE, BUSY, WARN, ERROR, Writable, Drivable, Communicator
from frappy.datatypes import StringType, BoolType, IntRange, NoneOr, Int32
from frappy.errors import CommunicationFailedError, ConfigError
from threading import RLock
@@ -80,11 +81,16 @@ class IO(Communicator):
class LogoMixin(HasIO):
ioclass = IO
def get_vm_value(self, vm_address):
return literal_eval(self.io.communicate(vm_address))
def get_vm_value(self, addr, scale=None):
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):
return literal_eval(self.io.communicate(f'{vm_address} {round(value)}'))
def set_vm_value(self, addr, value, scale=None):
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):
@@ -219,195 +225,47 @@ class DelayedActuator(DigitalActuator, Drivable):
self._pulse_end = now + delay
class Value(LogoMixin, Readable):
class Sensor(LogoMixin, Readable):
addr = Property('VM address', datatype=StringType())
scale = Property('scale to multiply with raw integer value',
NoneOr(FloatRange()), default=None)
def read_value(self):
return self.get_vm_value(self.addr)
return self.get_vm_value(self.addr, self.scale)
def read_status(self):
return IDLE, ''
class DigitalValue(Value):
value = Parameter('airpressure state', datatype=BoolType())
class AnalogOutput(Sensor, Writable):
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(LogoMixin, Drivable):
vm_address = Property('VM address', datatype=StringType())
class Pressure(Sensor):
value = Parameter('pressure', datatype=FloatRange(unit='mbar'))
# pollinterval = 0.5
def read_value(self):
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())
class Resistor(Sensor):
value = Parameter('resistance', datatype=FloatRange(unit='Ohm'))
def read_value(self):
return self.get_vm_value(self.vm_address)
def read_status(self):
return IDLE, ''
class HeaterParam(LogoMixin, Writable):
vm_address = Property('VM address output', datatype=StringType())
target = Parameter('Heater target', datatype=IntRange())
value = Parameter('Heater Param', datatype=IntRange())
class Comparator(LogoMixin, Readable):
addr = Property('VM address', datatype=StringType())
scale = Property('scale to multiply with raw integer value',
NoneOr(FloatRange()), default=None)
value = Parameter('airpressure state', datatype=BoolType())
threshold = Property('threshold for True', FloatRange())
def read_value(self):
return self.get_vm_value(self.vm_address)
def write_target(self, target):
return self.set_vm_value(self.vm_address, target)
def read_status(self):
return IDLE, ''
class controlHeater(LogoMixin, Writable):
vm_address = Property('VM address on switch', datatype=StringType())
target = Parameter('Heater state', datatype=BoolType())
value = Parameter('Heater state', datatype=BoolType())
def read_value(self):
return self.get_vm_value(self.vm_address_on)
def write_target(self, target):
if (target):
return self.set_vm_value(self.vm_address, True)
else:
return self.set_vm_value(self.vm_address, False)
def read_status(self):
return IDLE, ''
class safetyfeatureState(LogoMixin, Readable):
vm_address = Property('VM address state', datatype=StringType())
value = Parameter('safety Feature state', datatype=BoolType())
def read_value(self):
return self.get_vm_value(self.vm_address)
def read_status(self):
return IDLE, ''
class safetyfeatureParam(LogoMixin, Writable):
vm_address = Property('VM address output', datatype=StringType())
target = Parameter('safety Feature target', datatype=IntRange())
value = Parameter('safety Feature Param', datatype=IntRange())
def read_value(self):
return self.get_vm_value(self.vm_address)
def write_target(self, target):
return self.set_vm_value(self.vm_address, target)
def read_status(self):
return IDLE, ''
class comparatorgekoppeltParam(LogoMixin, Writable):
vm_address_1 = Property('VM address output', datatype=StringType())
vm_address_2 = Property('VM address output', datatype=StringType())
target = Parameter('safety Feature target', datatype=IntRange())
value = Parameter('safety Feature Param', datatype=IntRange())
def read_value(self):
return self.get_vm_value(self.vm_address_1)
def write_target(self, target):
self.set_vm_value(self.vm_address_1, target)
return self.set_vm_value(self.vm_address_2, target)
def read_status(self):
return IDLE, ''
return self.get_vm_value(self.addr, self.scale) > self.threshold

View File

@@ -64,8 +64,6 @@ def parse_result(reply):
class LakeShoreIO(HasIO):
ioClass = StringIO
def set_param(self, cmd, *args):
args = [f'{a:g}' for a in args]
if ' ' in cmd.strip():

View File

@@ -61,7 +61,6 @@ class SimpleMagfield(HasStates, Drivable):
'trained field (positive)',
TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')),
readonly=False, default=(0, 0))
trainmode = Parameter('train mode flag', EnumType(off=0, on=1, undef=2), default=2)
wait_stable_field = Parameter(
'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31)
ramp_tmo = Parameter(
@@ -150,24 +149,10 @@ class SimpleMagfield(HasStates, Drivable):
"""
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')
def ramp_to_target(self, sm):
if sm.init:
self.init_progress(sm, self.value)
self.handle_train_mode()
# Remarks: assume there is a ramp limiting feature
if abs(self.value - sm.target) > self.tolerance:
if self.get_progress(sm, self.value) > self.ramp_tmo:
@@ -181,15 +166,11 @@ class SimpleMagfield(HasStates, Drivable):
def stabilize_field(self, sm):
if sm.now - sm.stabilize_start < self.wait_stable_field:
return Retry
self.handle_train_mode()
return self.final_status()
def read_workingramp(self):
return self.ramp
def write_trainmode(self, value):
"""overwrite when needed"""
class Magfield(SimpleMagfield):
status = Parameter(datatype=StatusType(Status))
@@ -354,7 +335,6 @@ class Magfield(SimpleMagfield):
@status_code(Status.RAMPING)
def ramp_to_target(self, sm):
self.handle_train_mode()
dif = abs(self.value - sm.target)
if sm.init:
sm.stabilize_start = 0 # in case current is already at target
@@ -373,7 +353,6 @@ class Magfield(SimpleMagfield):
@status_code(Status.STABILIZING)
def stabilize_field(self, sm):
self.handle_train_mode()
if sm.now < sm.stabilize_start + self.wait_stable_field:
return Retry
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):
identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')]
timeout = 5
encoding = 'latin1'
@Command(StringType(), result=StringType(isUTF8=True))
def communicate(self, cmd, noreply=False):
return super().communicate(cmd, noreply)
class MercuryChannel(HasIO):
ioClass = IO
slot = Property('comma separated slot id(s), e.g. DB6.T1', StringType())
kind = '' #: used slot kind(s)
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
_lasttime = 0
_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
_history = None
__errcnt = 0
__inside_poll = False
__cache = None
@@ -113,7 +114,6 @@ class PImixin(HasOutputModule, Writable):
def initModule(self):
self.__cache = {}
self._overflow = np.zeros(10)
super().initModule()
if self.output_range != (0, 0): # legacy !
self.output_min, self.output_max = self.output_range
@@ -131,6 +131,13 @@ class PImixin(HasOutputModule, Writable):
self.__cache = {}
now = time.time()
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:
self._lastdiff = 0
return
@@ -143,34 +150,30 @@ class PImixin(HasOutputModule, Writable):
self._lastdiff = diff
deltadiff = diff - self._lastdiff
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 += self._overflow[-1] + (
output += self._overflow + (
self.p * deltadiff +
self.i * deltat * diff / self.time_scale) / self.input_scale
if omin <= output <= omax:
overflow = 0
self._overflow = 0
else:
# save overflow for next step
if output < omin:
overflow = output - omin
self._overflow = output - omin
output = omin
else:
overflow = output - omax
self._overflow = 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))
self.__errcnt = 0
except Exception as e:
@@ -184,10 +187,10 @@ class PImixin(HasOutputModule, Writable):
finally:
self.__inside_poll = False
self.__cache = {}
self.overflow = self._overflow[-1]
self.overflow = self._overflow
def write_overflow(self, value):
self._overflow.fill(value)
self._overflow = value
def internal_poll(self):
super().doPoll()

View File

@@ -114,7 +114,6 @@ class Main(Communicator):
class PpmsBase(HasIO, Readable):
"""common base for all ppms modules"""
ioClass = Main
value = Parameter(needscfg=False)
status = Parameter(datatype=StatusType(Readable, 'DISABLED'), needscfg=False)

View File

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

View File

@@ -479,7 +479,7 @@ def pop_cfg(cfgdict, *args):
class SeaModule(Module):
io = Attached(SeaClient)
io = Attached()
# pylint: disable=too-many-statements,arguments-differ,too-many-branches
def __new__(cls, name, logger, cfgdict, srv):

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

@@ -1,560 +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>
# *****************************************************************************
"""module to trigger plug and play mechanism
For all detected secop servers, setup files are written to the
setup directory.
For SEC nodes started on the instrument computer, wrapper config files
containing the port number are produced in the wrapper directory.
Mechanism:
start or detect a SecNode
(on start only) create a wrapper file and tell marche to start
add SecNode to superfrappy._secnodes in 'connecting' mode
action table:
connection == online, setupfile does not exist:
- announce = new, create wrapperfile (if needed) and setup file
nicos == idle, setup not in loaded_setups, announce == new:
- announce = None, plugplay = new
setup in loaded_setups, plugplay == new:
- pluglay = quiet (handled in _handle_setups)
connection != online, setup in loaded_setups:
- announce = cancel
nicos == idle, setup not in loaded_setups, announce == cancel:
- announce = None, plugplay = cancel
setup not in loaded_setups, plugplay == cancel:
- plugplay = quiet (handled in _handle_setups)
setup not in loaded_setups, plugplay == quiet and setupfile exists:
- remove setupfile and wrapper file (if present)
"""
import time
import socket
import re
from ast import literal_eval
from pathlib import Path
from frappy.lib import mkthread, formatExtendedTraceback
from frappy.core import Readable, Parameter, Property, Command, Communicator, HasIO
from frappy.datatypes import ArrayOf, StructOf, StringType, IntRange, TupleOf, BoolType
from frappy.client import SecopClient
from .marche_frappy import FrappyMarche
from .secop_udp import UdpScan, UdpListener
from .normalizeuri import normalizeuri
porttype = IntRange(0, 0xc000)
secnodetype = StructOf(cfg=StringType(), uri=StringType(), status=StringType())
SETUP_TEMPLATE = """description = 'frappy %(cfg)s setup'
group = 'plugplay'
devices = {
%(devname)s:
device('nicos_sinq.frappy_sinq.new.FrappyMarcheNode',
description='%(cfg)s SEC node', unit='', async_only=True,
prefix='se_', auto_create=True,
uri=%(uri)s,
general_stop_whitelist=['om', 'stickrot'],
%(nodeargs)s),
}
%(aliasconfig)s
"""
MEANINGS = {
'temperature': 'Ts',
'temperature_regulation': 'T',
'magneticfield': 'B',
'pressure': 'p',
'rotation_z': 'a3',
'stick_rotation': 'dom',
}
SKIP_ENV = 'rotation_z', 'dom'
class SecNode:
log = None
def __init__(self, host_port, cfg):
self.host_port = host_port
self.host, _, self.port = host_port.partition(':')
self.client = SecopClient(host_port)
self.nodename = cfg
self.cfg = cfg
self.description = cfg
self.online = False
self._status = 'created'
self.trigger = True
self.announce_pnp = True # None: do not announce, True: announce new, False: announce removal
self.setup_was_loaded = False
def connect(self, complete_callback=None, log=None):
self._status = 'connecting'
self.log = log
self.complete_callback = complete_callback
if self.log:
self.log.debug('spawn connect')
self.client.spawn_connect(self.complete)
def get_setup(self):
return f'se_{self.cfg}'
def complete(self):
try:
self.online = True
self._status = 'completing'
self.nodename = self.client.nodename
if self.log:
self.log.debug('connected to %r', self.nodename)
if self.complete_callback:
if not self.cfg:
if self.nodename == self.client.uri:
self.cfg = self.host_port
else:
self.cfg = self.nodename.replace('.', '_')
try:
self.complete_callback(self)
except Exception as e:
self.log.exception('complete_callback failed')
self.complete_callback = None
desc = self.client.properties.get('description') or self.nodename
self.description = desc.split('\n')[0]
self._status = 'connected'
except Exception as e:
self.log.exception('connect failed')
self._status = f'disconnected {e!r}'
def disconnect(self):
self.online = False
self._status = 'disconnecting'
self.client.disconnect()
self._status = 'disconnected'
def status(self):
return self._status
def noop(*args):
pass
MSGPAT = re.compile(r'([^=!]*)(?:([=!])(.*))?')
def noop(*args):
pass
def get_lookup_key(key):
split = key.split('/')
return f'/{split[-2]}/{split[-1]}' if len(split) > 2 else '/'.join(split[-2:])
class NicosCache(Communicator):
uri = Property('<host>:<port>>', StringType(), default='localhost')
_error = None
_sock = None
def doPoll(self):
self.log.info('doPoll')
if self._error:
self.log.error('%r', self._error)
self._error = None
@Command(argument=StringType())
def communicate(self, command):
"""send a command, do not wait for any response"""
self._connect()
self.log.info('> %r', command)
self._sock.sendall(command.encode() + b'\n')
def _connect(self, keys=()):
if self._sock is None:
host, _, port = self.uri.partition(':')
self._sock = socket.create_connection((host or 'localhost', int(port or 14869)))
for lookup_key in keys:
msg = f'{lookup_key}*\n{lookup_key}:\n'
self._sock.sendall(msg.encode())
def recvloop(self, handlers, other=noop, exception=noop):
handler_lookup = {}
for key, handler in handlers.items():
pat = re.compile(key.replace('*', '.*'))
lookup_key = get_lookup_key(key)
handler_lookup.setdefault(lookup_key, []).append((pat, handler))
while True:
try:
self._connect(handler_lookup)
self._error = 'connected'
except Exception:
self._error = formatExtendedTraceback()
time.sleep(10)
continue
try:
buffer = b''
while True:
raw = self._sock.recv(8192)
if not raw:
break
messages = (buffer + raw).split(b'\n')
buffer = messages.pop()
for msg in messages:
msg = msg.strip().decode()
match = MSGPAT.match(msg)
if not match:
other(msg)
continue
key, op, value = match.groups()
lookup_key = get_lookup_key(key)
for pat, handler in handler_lookup.get(lookup_key, ()):
if pat.match(key):
try:
if value is not None:
value = literal_eval(value)
handler(key, op, value)
except Exception as e:
self._error = formatExtendedTraceback()
exception(e)
break
else:
other(key)
messages.extend(raw.split(b'\n'))
except Exception as e:
self._error = formatExtendedTraceback()
self._sock = None
exception(e)
raise
class SuperFrappy(HasIO, Readable):
ioClass = NicosCache
marcheport = Property('marche port number', porttype, default=8124)
is_main_instrument = Property('this is the main instrument', BoolType(), default=True)
value = Parameter('running servers', ArrayOf(secnodetype), default=())
instance = Parameter('"main" or <instrument>', StringType())
nicos_setups = Parameter('active nicos se setups', ArrayOf(StringType()))
_marche = None
_secnodes = None # dict <host_post> of SecNode
_udp_listener = None
_nicos_idle_since = None
_setups = ()
_to_close = ()
def initModule(self):
super().initModule()
self._marche = FrappyMarche(self.instance, 'localhost', self.marcheport)
self.setupdir = self._marche.config['setupdir']
self.wrapperdir = self._marche.wrapperdir
self._secnodes = {}
self._udp_listener = [UdpScan(True), UdpListener(True)]
self.rescan()
self._announced_setups = {}
self._current_plugplay = {}
self._to_close = set()
# self.log.info('%r', self.log.handlers[0].setLevel(10))
mkthread(self.io.recvloop, {
'nicos/session/mastersetupexplicit': self._handle_setups,
'nicos/exp/scripts': self._handle_scripts,
'se/*/nicos/setupname': self._handle_plugplay,
})
def _cache_send(self, key, op, value=''):
if op in ('=', '!'):
value = repr(value)
self.io.communicate(key + op + value)
def _handle_scripts(self, key, op, value):
self.log.debug('scripts %r', value)
if value:
self._nicos_idle_since = None
self.setFastPoll(False)
else:
if not self._nicos_idle_since:
self._nicos_idle_since = time.time()
self.setFastPoll(True, 0.25)
def _handle_setups(self, key, op, value):
self._setups = set(value)
self.log.debug('setups %r', value)
for secnode in self._secnodes.values():
setup = secnode.get_setup()
loaded = setup in self._setups
pnp = self._current_plugplay.get(secnode.nodename)
if loaded:
if pnp is True:
self._send_pnp_message(secnode.nodename, None, True)
secnode.setup_was_loaded = True
else:
if pnp is False:
self._send_pnp_message(secnode.nodename, None, False)
if secnode.setup_was_loaded:
self._to_close.add(secnode.host_port)
def _handle_plugplay(self, key, op, value):
value = None if value is None else (op == '=')
if value is None:
self._current_plugplay.pop(key, None)
else:
self._current_plugplay[key] = value
self.log.debug('pnp %r', self._current_plugplay)
def _write_setup_file(self, secnode):
setup_file = Path(self.setupdir) / f'{secnode.get_setup()}.py'
envlist, alias_config, devmap = self.node_setup_info(secnode)
nodeargs = f'device_mapping={devmap!r}' if devmap else ''
aliasconfig = f'alias_config = {alias_config!r}' if alias_config else ''
setup_content = SETUP_TEMPLATE % {
'cfg': secnode.cfg, 'devname': f'"secnode_{secnode.cfg}"',
'uri': repr(secnode.host_port), 'nodeargs': nodeargs, 'aliasconfig': aliasconfig}
setup_file.write_text(setup_content)
def _send_pnp_message(self, nodename, setup, on):
self._current_plugplay[nodename] = None if setup is None else on
self._cache_send(f'se/{nodename}/nicos/setupname', '=' if on else '!', setup)
def _update(self):
while self._to_close:
secnode = self._secnodes.pop(self._to_close.pop(), None)
self._remove_secnode(secnode)
superfluous_setup_files = set(Path(self.setupdir).glob('*.py'))
superfluous_cfg_files = set(Path(self.wrapperdir).glob('*_cfg.py'))
for secnode in self._secnodes.values():
nodename = secnode.nodename.replace('/', '_')
if not secnode.cfg:
continue
setup = secnode.get_setup()
setup_file = Path(self.setupdir) / f'{setup}.py'
superfluous_setup_files.discard(setup_file)
superfluous_cfg_files.discard(Path(self.wrapperdir) / f'{secnode.cfg}_cfg.py')
if setup in self._setups:
if not secnode.online:
if secnode.announce_pnp is False:
if self._nicos_idle_since:
secnode.announce_pnp = None
self._send_pnp_message(nodename, setup, False)
elif secnode.host_port not in self._to_close:
secnode.announce_pnp = False
else:
if secnode.online:
if not setup_file.is_file():
self.log.debug('write_setup %r', secnode.cfg)
self._write_setup_file(secnode)
if secnode.announce_pnp is True and self._nicos_idle_since:
secnode.announce_pnp = None
self._send_pnp_message(nodename, setup, True)
self._to_close.discard(secnode.host_port)
for setup in self._setups:
# do not delete active setups
superfluous_setup_files.discard(Path(self.setupdir) / f'{setup}.py')
reload_marche = bool(superfluous_cfg_files)
for file in superfluous_setup_files | superfluous_cfg_files:
self.log.debug('remove %s', file)
try:
file.unlink()
except FileNotFoundError:
pass
if reload_marche:
self._marche.reload()
def doPoll(self):
super().doPoll()
for listener in self._udp_listener:
msg = listener.poll(self.log)
if msg:
uri = msg['uri']
if uri in self._secnodes:
self.log.debug('%r is already known', msg)
else:
self.log.debug('%r appeared', msg)
cfg = msg.get('device', '')
if uri.startswith('localhost:') and cfg != 'superfrappy':
self.connect(uri)
now = time.time()
if now > (self._nicos_idle_since or now) + 0.5:
self.setFastPoll(False)
self._update()
@Command
def rescan(self):
"""rescan secop servers in subnet"""
for listener in self._udp_listener:
listener.start()
def read_value(self):
value = []
for secnode in self._secnodes.values():
info = {'cfg': secnode.cfg, 'uri': secnode.host_port, 'status': secnode.status()}
value.append(info)
return value
def read_nicos_setups(self):
return list(self._setups & {s.get_setup() for s in self._secnodes.values()})
@Command()
def ping(self):
"""nicos cache ping"""
self._cache_send('#ping#', '?', '')
@Command(argument=StringType())
def restart(self, cfg):
"""restart the frappy server
works only when it runs on the local machine
"""
self._marche.restart(cfg)
@Command(argument=StringType())
def connect(self, host_port):
"""connect secnode
- add a setup file to the setup dir
:param host_port: box address <host>:<port>
"""
host_port = normalizeuri(host_port, True)
secnode = self._secnodes.get(host_port)
if secnode:
self.log.debug('already connected %r', host_port)
else:
self.log.debug('connect secnode %r', host_port)
secnode = SecNode(host_port, self._marche.cfg_info.get(host_port, ''))
self._secnodes[host_port] = secnode
secnode.connect(log=self.log)
self.read_value()
@Command(argument=StringType())
def main(self, cfg):
"""add or change main cfg"""
self._add_secnode('main', cfg)
@Command(argument=StringType())
def stick(self, cfg):
"""add or change stick cfg"""
self._add_secnode('stick', cfg)
@Command(argument=StringType())
def add(self, cfg):
"""add an addons cfg"""
self._add_secnode('addons', cfg)
def _add_secnode(self, service, cfg):
"""add and start server on localhost
- add a wrapper cfg file to the wrapper dir
- add a setup file to the setup dir
:param service: 'stick', 'main', '' or a stringified port number
:param cfg: config file or equipment id
"""
self.log.debug('add and start %r', cfg)
port = self._marche.get_port(service)
host_port = f'localhost:{port}'
secnode = SecNode(host_port, cfg)
self._secnodes[host_port] = secnode
self._marche.add_frappy_service(service, secnode.cfg, secnode.port, self.log)
self.log.debug('start %r at %r', secnode.cfg, host_port)
self._marche.start(secnode.cfg)
secnode.connect(log=self.log)
self.read_value()
def _remove_secnode(self, secnode):
secnode.announce = False
secnode.disconnect()
self.log.debug('secnode.host %r', secnode)
if secnode.host == 'localhost':
self._marche.stop(secnode.cfg)
self._update()
self.read_value()
@Command(argument=StringType())
def remove(self, host_port_or_cfg):
"""remove server
- for servers on localhost stop the server
- register the setup file for deletion
"""
by_cfg = [s for s in self._secnodes.values() if s.cfg == host_port_or_cfg]
if by_cfg:
secnode = by_cfg[0]
else:
host_port = normalizeuri(host_port_or_cfg, True)
secnode = self._secnodes.get(host_port)
if secnode:
self._remove_secnode(secnode)
self._update()
self.read_value()
else:
raise ValueError(f'no running server found for {host_port_or_cfg}')
@Command(argument=TupleOf(StringType(), porttype), result=StringType())
def running_cfg(self, host_port):
"""when running, return cfg, else an empty string"""
secnode = self._secnodes.get(host_port)
if secnode and secnode.online:
return secnode.cfg
return ''
def node_setup_info(self, secnode):
"""create aliases and envlist for SECoP devices
depending on their meaning
"""
modules = secnode.client.modules
result = {} # dict <meaning name> of list of (<importance>, <target>)
device_mapping = {}
reserved_names = {v.lower() for v in MEANINGS.values()}
for modname, moddesc in modules.items():
if modname.lower() in reserved_names:
device_mapping[modname] = {'name': f'{modname}_'}
meaning = moddesc['properties'].get('meaning')
if meaning:
meaning_name, importance = meaning
if meaning_name not in MEANINGS:
self.log.warning('%s: meaning %r is unknown', modname, meaning_name)
continue
result.setdefault(meaning_name, []).append((importance, modname))
if meaning_name == 'temperature_regulation':
# add temperature_regulation to temperature list, with very low importance
result.setdefault('temperature', []).append((importance - 100, modname))
elif meaning_name == 'temperature' and moddesc['parameters'].get('target'):
result.setdefault('temperature_regulation', []).append((importance, modname))
envlist = []
alias_config = {}
for meaning_name, info in result.items():
importance, modname = sorted(info)[-1]
target = MEANINGS.get(meaning_name)
alias_config[target] = {modname: importance}
if target == 'a3' and meaning_name == 'rotation_z':
alias_config['om'] = {modname: importance}
if target not in SKIP_ENV:
envlist.append(target)
return envlist, alias_config, device_mapping

View File

@@ -1,271 +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 sys
import time
import re
import socket
from pathlib import Path
from configparser import ConfigParser
import logging
MARCHESRC = ['/home/software/marche']
CFGDIRS = ['/home/linse/config', '/home/l_samenv/linse_config']
def get_logger(previous=[]):
if previous:
return previous[0]
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))
previous.append(logger)
return logger
for marchedir in MARCHESRC:
if Path(marchedir).is_dir():
sys.path.append(marchedir)
import marche.jobs as mj
from marche.client import Client
break
STATUS_MAP = { # values are (<busy: bool), <target state:bool>, name)
mj.DEAD: (False, False, 'DEAD'),
mj.NOT_RUNNING: (False, False, 'NOT RUNNING'),
mj.STARTING: (True, True, 'STARTING'),
mj.INITIALIZING: (True, True, 'INITIALIZING'),
mj.RUNNING: (False, True, 'RUNNING'),
mj.WARNING: (False, True, 'WARNING'),
mj.STOPPING: (True, False, 'STOPPING'),
mj.NOT_AVAILABLE: (False, False, 'NOT AVAILABLE'),
}
def wait_status(cl, service):
delay = 0.2
while True:
sts = cl.getServiceStatus(service)
if STATUS_MAP[sts][0]: # busy
if delay > 1.5: # this happens after about 5 sec
return False
time.sleep(delay)
delay *= 1.225
continue
return True
class MarcheControl:
port = 8124
def __init__(self, host, port=None, user=None, instrument=None):
self.host = host
self.instrument = instrument or socket.gethostname().split('.')[0]
self.user = user or instrument # SINQ instruments
if port is not None:
self.port = port
self._client = None
def connect(self):
if self._client is None:
# TODO: may need more generic solution for last arg
print(self.host, self.port, self.user)
self._client = Client(self.host, self.port, self.user, self.instrument.upper() + 'LNS')
# TODO; do we need disconnect?
def get_service(self, instance):
return instance
def start(self, instance):
self.connect()
print(self.get_service(instance))
self._client.startService(self.get_service(instance))
def restart(self, instance):
self.connect()
self._client.restartService(self.get_service(instance))
def stop(self, instance):
self.connect()
self._client.stopService(self.get_service(instance))
def status(self, service):
"""returns a dict <service> of (<busy>, <running (now or soon)>, <state name>)"""
self.connect()
servdict = self._client.getAllServiceInfo().get(service)
if not servdict:
return {}
statedict = servdict['instances']
return {k: STATUS_MAP[v['state']] for k, v in statedict.items()}
def reload(self):
self.connect()
self._client.reloadJobs()
def run(self, action, instance=None, *args):
if action == 'start':
self.start(instance)
elif action == 'restart':
self.restart(instance)
elif action == 'stop':
self.stop(instance)
else:
raise ValueError('unknown args %r', (action, instance) + args)
WRAPPER_CFG = """interface = '{port}'
include({cfg!r})
overrideNode(interface=interface)
"""
WRAPPER_PAT = re.compile(r"interface\s=\s*'(\d*)'\s*\n")
class FrappyMarche(MarcheControl):
def __init__(self, instance, host='localhost', port=None, user=None):
parser = ConfigParser()
parser.optionxform = str
gencfg = f'~/.config/frappy/{instance}.cfg'
parser.read([str(Path(gencfg).expanduser())])
try:
section = dict(parser['superfrappy'])
except KeyError:
raise ValueError(f'bad config {gencfg}')
self.instance = instance # 'main' or an instrument on a generic computer
instrument = section.get('instrument', instance)
if instrument == 'main':
instrument = socket.gethostname().split('.')[0]
print(instance, instrument)
self.instrument = instrument # the instrument name
self.config = {k: section[k].replace('<INS>', instrument) for k in section}
self.wrapperdir = self.config.pop('wrapperdir')
self.cfgdirs = self.config.pop('cfgdirs')
self.main_port = int(self.config.pop('main_port'))
if not Path(self.wrapperdir).is_dir():
raise ValueError(f'{self.wrapperdir} does not exist')
self.get_cfg_info() # do we need to update this from time to time?
super().__init__(host, port, user, instrument)
def get_service(self, instance):
return f'frappy.{instance}' if self.instance == 'main' else f'frappy.{self.instrument}-{instance}'
def wrapper_file(self, cfg):
return Path(self.wrapperdir) / f'{cfg}_cfg.py'
def cfg_file(self, cfgdirs, service, cfg):
cfgpy = f'{cfg}_cfg.py'
tries = []
for servicedir in (service, ''):
for cfgdir in cfgdirs.split(':'):
cfgfile = Path(cfgdir) / servicedir / cfgpy
tries.append(cfgfile)
if cfgfile.is_file():
return cfgfile
else:
raise FileNotFoundError(f'can not find {cfgpy} in {tries}')
def get_std_port(self, service):
port = self.main_port
if service == 'main':
return port
port += 1
if service == 'stick':
return port
return port + 1, self.main_port + 10
def get_local_ports(self):
self.get_cfg_info()
run_state = self.status('frappy')
result = {}
for host_port, cfg in self.cfg_info.items():
host, port = host_port.split(':')
if host == 'localhost':
busy, on, _ = run_state.get(cfg, (0,0,0))
if on or busy:
result.setdefault(port, []).append((on, busy, cfg))
return {sorted(v)[-1][-1]: k for k, v in result.items()}
def get_port(self, service):
if service not in {'main', 'stick', 'addons', 'addon'}:
raise ValueError('illegal service argument')
ports = self.get_std_port(service)
if isinstance(ports, int):
return ports
self.get_cfg_info()
used_ports = self.get_local_ports()
for port in range(*ports):
if port not in used_ports:
return port
raise ValueError('too many frappy servers')
def add_frappy_service(self, service, cfg, port, log=None):
if log is None:
log = get_logger()
log.info('add %r port=%r', cfg, port)
cfgfile = self.cfg_file(self.cfgdirs, service, cfg)
wrapper_content = WRAPPER_CFG.format(cfg=str(cfgfile), port=port)
self.wrapper_file(cfg).write_text(wrapper_content)
self.get_cfg_info()
log.info('wrapper %r %r', self.wrapper_file(cfg), wrapper_content)
self.reload()
log.info('registered %r', cfg)
def get_cfg_info(self):
"""get info from wrapper dir"""
result = {}
for cfgfile in Path(self.wrapperdir).glob('*_cfg.py'):
cfg = cfgfile.stem[:-4]
match = WRAPPER_PAT.match(cfgfile.read_text())
if match:
result[f'localhost:{match.group(1)}'] = cfg
self.cfg_info = result
def delete_frappy_service(self, cfg):
try:
self.wrapper_file(cfg).unlink()
self.reload()
except FileNotFoundError:
pass
def run(self, action, *args):
if action == 'start':
try:
service, cfg = args
except ValueError:
raise ValueError('start needs <service> <cfg>')
port = self.get_port(service)
self.add_frappy_service(service, cfg, port)
self.start(cfg)
elif action == 'restart':
if len(args) != 1 or args[0] in {'main', 'stick', 'addons', 'addon'}:
raise ValueError('restart needs <cfg>')
self.restart(args[0])
elif action == 'stop':
if len(args) != 1 or args[0] in {'main', 'stick', 'addons', 'addon'}:
raise ValueError('stop needs <cfg>')
self.delete_frappy_service(args[0])
self.stop(args[0])
else:
raise ValueError('unknown action %r', action)

View File

@@ -1,41 +0,0 @@
import re
import socket
# sorry for hardwiring this ... there is no CNAME reverse lookup!
# taking the original address as unique name would need a call to
# gethostbyaddr, which might take some time - also not what we want
reverse_alias = {
'pc15139': 'linse-c',
'pc16392': 'linse-a',
}
def normalizeuri(uri, use_localhost=False):
host, sep, port = uri.partition(':')
if host[0].isdigit():
if not port and '.' not in host: # assume this is a port number
host, sep, port = 'localhost', ':', host
else:
try:
socket.setdefaulttimeout(1)
host = socket.gethostbyaddr(host)[0].split('.', 1)[0]
except socket.gaierror:
pass # keep numbered IP
finally:
socket.setdefaulttimeout(None)
else:
host = host.split('.', 1)[0]
hostname = socket.gethostname().split('.')[0]
if use_localhost:
if host in (hostname, reverse_alias.get(hostname, hostname)):
host = 'localhost'
else:
if host == 'localhost':
host = hostname
host = reverse_alias.get(host, host)
# strip appended IP when a host is registered twice (at PSI):
match = re.match(r'([^-]+)-129129\d{6}$', host)
host = match.group(1) if match else host
return f'{host}{sep}{port}'

View File

@@ -1,115 +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 os
import time
import socket
import json
from select import select
from .normalizeuri import normalizeuri
SECOP_UDP_PORT = 10767
class Listener:
socket = None
def __init__(self, use_localhost=False):
self.use_localhost = use_localhost # whether 'localhost' or the real hostname is returned on the own machine
def poll(self, log=None):
if self.socket is None:
return None
if not select([self.socket], [], [], 0)[0]:
return None
try:
msg, addr = self.socket.recvfrom(1024)
except socket.error: # pragma: no cover
return None
addr = socket.getnameinfo(addr, socket.NI_NOFQDN)[0]
msg = json.loads(msg.decode('utf-8'))
if log:
log.debug('got msg %r', msg)
kind = msg.pop('SECoP', None)
if kind == 'node':
msg['device'] = msg['equipment_id'].split('.')[0]
uri = f"{addr}:{msg['port']}"
elif kind == 'for_other_node':
uri = msg['uri']
else:
return None
host, _, port = uri.rpartition(':')
host = normalizeuri(host or 'localhost', self.use_localhost)
msg['uri'] = f'{host}:{port}'
return msg
class UdpScan(Listener):
def start(self, log=None):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# send a general broadcast
try:
sock.sendto(json.dumps(dict(SECoP='discover')).encode('utf-8'),
('255.255.255.255', SECOP_UDP_PORT))
except OSError as e:
if log:
log.info('could not send the broadcast %r:', e)
self.socket = sock
self.deadline = time.time() + 30
def poll(self, log=None):
if self.socket is None:
return None
if time.time() > self.deadline:
try:
self.socket.close()
except Exception:
pass
self.socket = None
return super().poll(log)
class UdpListener(Listener):
def start(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1)
if os.name == 'nt':
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
else:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.bind(('0.0.0.0', SECOP_UDP_PORT))
self.socket = sock
def send_other_udp(uri, instrument, device=None):
"""inform the feeder about the start of a frappy server"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
msg = {
'SECoP': 'for_other_node',
'uri': uri,
'instrument': instrument,
}
if device:
msg['device'] = device
msg = json.dumps(msg, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
sock.sendto(msg, ('255.255.255.255', SECOP_UDP_PORT))

View File

@@ -30,7 +30,6 @@ class IO(StringIO):
class Power(HasIO, Readable):
ioClass = IO
value = Parameter(datatype=FloatRange(0,3300,unit='W'))
voltage = Parameter('voltage', FloatRange(0,8, unit='V'))
current = Parameter('current', FloatRange(0,400, unit='A'))
@@ -42,7 +41,6 @@ class Power(HasIO, Readable):
class Output(HasIO, Writable):
ioClass = IO
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
target = Parameter(datatype=FloatRange(0,100,unit='%'))
mode = Parameter('regulation mode', EnumType(voltage=1, current=2, both=3),

View File

@@ -162,9 +162,7 @@ class Motor(PersistentMixin, HasIO, Drivable):
if self.has_home:
self.parameters['home'].export = '_home'
self.setProperty('has_inputs', True)
if self.has_inputs:
self.parameters['input3'].export = '_input3'
else:
if not self.has_inputs:
self.setProperty('with_pullup', False)
def writeInitParams(self):

View File

@@ -1,169 +0,0 @@
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
-------------
x
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.
BlaBla 1
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 2
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 3
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 4
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 5
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 6
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 7
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 8
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 9
--------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
BlaBla 10
---------
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla
bla bla

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