Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bb869b43e | |||
| 3ede9eb9f4 | |||
| f57400feb9 |
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
94
cfg/gas10ka_cfg.py
Normal 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',
|
||||
)
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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 = [],
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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`)
|
||||
@@ -140,4 +140,4 @@ Exception classes
|
||||
.. automodule:: frappy.errors
|
||||
:members:
|
||||
|
||||
.. include:: configuration.inc
|
||||
.. include:: configuration.rst
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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='')
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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) )
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?')
|
||||
|
||||
@@ -45,7 +45,6 @@ class SR830_IO(StringIO):
|
||||
|
||||
|
||||
class StanfRes(HasIO, Readable):
|
||||
ioClass = SR830_IO
|
||||
def set_par(self, cmd, *args):
|
||||
"""
|
||||
Set parameter.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -68,7 +68,6 @@ class BridgeIO(StringIO):
|
||||
|
||||
|
||||
class Base(HasIO):
|
||||
ioClass = BridgeIO
|
||||
port = Property('modules port', IntRange(0, 15))
|
||||
|
||||
def communicate(self, command):
|
||||
|
||||
@@ -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
|
||||
53
frappy_psi/capillary_heater.py
Normal file
53
frappy_psi/capillary_heater.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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}'))
|
||||
|
||||
@@ -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')
|
||||
@@ -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': '',
|
||||
}
|
||||
@@ -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?
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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
290
frappy_psi/ips_classic.py
Normal 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
|
||||
@@ -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, ''
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, ''
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ class PulseIO(StringIO):
|
||||
|
||||
|
||||
class Base(HasIO):
|
||||
ioClass = PulseIO
|
||||
|
||||
def set_source(self):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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}'
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user