Compare commits
117 Commits
Author | SHA1 | Date | |
---|---|---|---|
f6a5ef8f4d | |||
dad9536eb5 | |||
ccc66468d4 | |||
52215f9ec1 | |||
58549065fb | |||
0230641b1d | |||
b264455ad3 | |||
07c5b32c5f | |||
![]() |
80cb3f08d7 | ||
fb4755502b | |||
3580cb9dc0 | |||
d681507f94 | |||
e0bd84cc3b | |||
9545cb4188 | |||
1fead8b2c6 | |||
809eda314b | |||
ca6fd1dd5e | |||
d0c063c60b | |||
7a59cf4956 | |||
7254d7f95c | |||
c368292873 | |||
6a2aece383 | |||
ad76a5d752 | |||
42e40db14b | |||
343ce90321 | |||
75783b211a | |||
![]() |
36f2919ec2 | ||
7cca3192df | |||
a632c53405 | |||
a76425cb2e | |||
d231e9ce06 | |||
44750572d9 | |||
e0ef6047e2 | |||
421eb67b93 | |||
3048b8cb7d | |||
0ef484e082 | |||
8560384529 | |||
![]() |
16d419c0f3 | ||
![]() |
8c548da2e0 | ||
![]() |
d9f340dce6 | ||
![]() |
1325c8924d | ||
![]() |
f8e3bd9ad2 | ||
6f547f0781 | |||
![]() |
322cd39e0a | ||
![]() |
41b51b35fd | ||
19571ab83d | |||
b35c97f311 | |||
5d175b89ca | |||
f8c52af3ac | |||
bf9c946b1d | |||
09e596f847 | |||
![]() |
7e2ccd214e | ||
907a52ccdb | |||
51dba895a5 | |||
![]() |
d86718b81e | ||
![]() |
42a6bfb5d2 | ||
895f66f713 | |||
3663c62b46 | |||
![]() |
8c2588a5ed | ||
![]() |
95dc8b186e | ||
![]() |
265dbb1a57 | ||
73e9c8915b | |||
2e99e45aea | |||
b7bc81710d | |||
eee63ee3df | |||
fd43687465 | |||
a25a368491 | |||
4397d8db1a | |||
e60ac5e655 | |||
0b5b40cfba | |||
2a617fbaf0 | |||
72d09ea73a | |||
1ae19d03b3 | |||
41cb107f50 | |||
8b0c4c78a9 | |||
7ac10d2260 | |||
6cbb3a094b | |||
![]() |
405d316568 | ||
![]() |
ac92a6ca3d | ||
![]() |
a9e3489325 | ||
654a472a7e | |||
![]() |
ddc72d0ea7 | ||
ede07e266c | |||
4b543d02a0 | |||
a4d5d8d3b7 | |||
b37e625df3 | |||
1dbd7c145a | |||
2aa27f1ea5 | |||
b28cdefe8a | |||
e0e442814f | |||
66895f4f82 | |||
49bf0d21a9 | |||
e8cd193d0d | |||
142add9109 | |||
![]() |
c2673952f4 | ||
![]() |
9fc2aa65d5 | ||
09fbaedb16 | |||
![]() |
5deaf4cfd9 | ||
81f7426739 | |||
![]() |
c69e516873 | ||
![]() |
64732eb0c8 | ||
![]() |
1535448090 | ||
![]() |
554996ffd3 | ||
![]() |
2d824978a9 | ||
![]() |
35dd166fee | ||
![]() |
aee99df2d0 | ||
8e05090795 | |||
![]() |
eac58982d9 | ||
![]() |
0f34418435 | ||
![]() |
1423800ff4 | ||
![]() |
e333763105 | ||
![]() |
c09e02a01e | ||
![]() |
337be1b2bc | ||
![]() |
752942483f | ||
0204bdfe2f | |||
facaca94eb | |||
0f0a177254 |
@ -69,7 +69,7 @@ def main(argv=None):
|
||||
console.setLevel(loglevel)
|
||||
logger.addHandler(console)
|
||||
|
||||
app = QApplication(argv)
|
||||
app = QApplication(argv, organizationName='frappy', applicationName='frappy_gui')
|
||||
|
||||
win = MainWindow(args, logger)
|
||||
app.aboutToQuit.connect(win._onQuit)
|
||||
|
@ -23,12 +23,12 @@
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.logging import logger
|
||||
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))
|
||||
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.logging import logger
|
||||
from frappy.client.interactive import Console
|
||||
from frappy.playground import play, USAGE
|
||||
|
||||
|
@ -59,16 +59,23 @@ def decode(msg, addr):
|
||||
|
||||
|
||||
def print_answer(answer, *, short=False):
|
||||
try:
|
||||
hostname = socket.gethostbyaddr(answer.address)[0]
|
||||
address = hostname
|
||||
numeric = f' ({answer.address})'
|
||||
except Exception:
|
||||
address = answer.address
|
||||
numeric = ''
|
||||
if short:
|
||||
# NOTE: keep this easily parseable!
|
||||
print(f'{answer.equipment_id} {answer.address}:{answer.port}')
|
||||
print(f'{answer.equipment_id} {address}:{answer.port}')
|
||||
return
|
||||
print(f'Found {answer.equipment_id} at {answer.address}:')
|
||||
print(f'Found {answer.equipment_id} at {address}{numeric}:')
|
||||
print(f' Port: {answer.port}')
|
||||
print(f' Firmware: {answer.firmware}')
|
||||
desc = answer.description.replace('\n', '\n ')
|
||||
print(f' Node description: {desc}')
|
||||
print()
|
||||
print('-' * 80)
|
||||
|
||||
|
||||
def scan(max_wait=1.0):
|
||||
@ -119,10 +126,14 @@ def listen(*, short=False):
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-l', '--listen', action='store_true',
|
||||
help='Print short info. '
|
||||
'Keep listening after the broadcast.')
|
||||
help='Keep listening after the broadcast.')
|
||||
parser.add_argument('-s', '--short', action='store_true',
|
||||
help='Print short info (always on when listen).')
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
short = args.listen or args.short
|
||||
if not short:
|
||||
print('-' * 80)
|
||||
for answer in scan():
|
||||
print_answer(answer, short=args.listen)
|
||||
print_answer(answer, short=short)
|
||||
if args.listen:
|
||||
listen(short=args.listen)
|
||||
listen(short=short)
|
||||
|
53
bin/peus-plot
Executable file
53
bin/peus-plot
Executable file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))
|
||||
|
||||
from frappy.client.interactive import Client
|
||||
from frappy_psi.iqplot import Plot
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Usage: peus-plot <maxY>')
|
||||
|
||||
|
||||
def get_modules(name):
|
||||
return list(filter(None, (globals().get(name % i) for i in range(10))))
|
||||
|
||||
|
||||
secnode = Client('pc13252:5000')
|
||||
time_size = {'time', 'size'}
|
||||
int_mods = [u] + get_modules('roi%d')
|
||||
t_rois = get_modules('roi%d')
|
||||
i_rois = get_modules('roi%di')
|
||||
q_rois = get_modules('roi%dq')
|
||||
|
||||
maxx = None
|
||||
if len(sys.argv) > 1:
|
||||
maxy = float(sys.argv[1])
|
||||
if len(sys.argv) > 2:
|
||||
maxx = float(sys.argv[2])
|
||||
else:
|
||||
maxy = 0.02
|
||||
|
||||
|
||||
iqplot = Plot(maxy, maxx)
|
||||
|
||||
for i in range(99):
|
||||
pass
|
||||
|
||||
try:
|
||||
while True:
|
||||
curves = np.array(u.get_curves())
|
||||
iqplot.plot(curves,
|
||||
rois=[(r.time - r.size * 0.5, r.time + r.size * 0.5) for r in int_mods],
|
||||
average=([r.time for r in t_rois],
|
||||
[r.value for r in i_rois],
|
||||
[r.value for r in q_rois]))
|
||||
if not iqplot.pause(0.5):
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
iqplot.close()
|
65
bin/us-plot
Executable file
65
bin/us-plot
Executable file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))
|
||||
|
||||
from frappy.client.interactive import Client
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from frappy_psi.iqplot import Pause
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("""
|
||||
Usage:
|
||||
|
||||
us-plot <end> [<start> [<npoints>]]
|
||||
|
||||
end: end of window [ns]
|
||||
start: start of window [n2], default: 0
|
||||
npoints: number fo points (default 1000)
|
||||
""")
|
||||
sys.exit(0)
|
||||
|
||||
Client('pc13252:5000')
|
||||
|
||||
|
||||
def plot(array, ax, style, xs):
|
||||
xaxis = np.arange(len(array)) * xs
|
||||
return ax.plot(xaxis, array, style)[0]
|
||||
|
||||
|
||||
def update(array, line, xs):
|
||||
xaxis = np.arange(len(array)) * xs
|
||||
line.set_data(np.array([xaxis, array]))
|
||||
|
||||
def on_close(event):
|
||||
sys.exit(0)
|
||||
|
||||
start = 0
|
||||
end = float(sys.argv[1])
|
||||
npoints = 1000
|
||||
if len(sys.argv) > 2:
|
||||
start = float(sys.argv[2])
|
||||
if len(sys.argv) > 3:
|
||||
npoints = float(sys.argv[3])
|
||||
|
||||
fig, ax = plt.subplots(figsize=(15,3))
|
||||
pause = Pause(fig)
|
||||
try:
|
||||
get_signal = iq.get_signal
|
||||
print('plotting RUS signal')
|
||||
except NameError:
|
||||
get_signal = u.get_signal
|
||||
print('plotting PE signal')
|
||||
|
||||
xs, signal = get_signal(start, end, npoints)
|
||||
|
||||
lines = [plot(s, ax, '-', xs) for s in signal]
|
||||
|
||||
while pause(0.5):
|
||||
plt.draw()
|
||||
xs, signal = get_signal(start, end, npoints)
|
||||
for line, sig in zip(lines, signal):
|
||||
update(sig, line, xs)
|
67
cfg/PEUS.py
67
cfg/PEUS.py
@ -1,67 +0,0 @@
|
||||
Node(equipment_id = 'pe_ultrasound.psi.ch',
|
||||
description = 'pulse echo ultra sound setup',
|
||||
interface = 'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
cls = 'frappy_psi.ultrasound.Frequency',
|
||||
description = 'ultrasound frequency and acquisition loop',
|
||||
uri = 'serial:///dev/ttyS1',
|
||||
pars = 'pars',
|
||||
pollinterval = 0.1,
|
||||
time = 900, # start time
|
||||
size = 5000,
|
||||
freq = 1.17568e+06,
|
||||
basefreq = 4.14902e+07,
|
||||
control = False,
|
||||
rusmode = False,
|
||||
amp = 5.0,
|
||||
nr = 1000, #500 #300 #100 #50 #30 #10 #5 #3 #1 #1000 #500 #300 #100 #50 #30 #10 #5 #3 #1 #500
|
||||
sr = 32768, #16384
|
||||
plot = True,
|
||||
maxstep = 100000,
|
||||
bw = 10E6, #butter worth filter bandwidth
|
||||
maxy = 0.7, # y scale for plot
|
||||
curves = 'curves', # module to transmit curves:
|
||||
)
|
||||
|
||||
Mod('curves',
|
||||
cls = 'frappy_psi.ultrasound.Curves',
|
||||
description = 't, I, Q and pulse arrays for plot',
|
||||
)
|
||||
|
||||
Mod('delay',
|
||||
cls = 'frappy__psi.dg645.Delay',
|
||||
description = 'delay line with 2 channels',
|
||||
uri = 'serial:///dev/ttyS2',
|
||||
on1 = 1e-9,
|
||||
on2 = 1E-9,
|
||||
off1 = 400e-9,
|
||||
off2 = 600e-9,
|
||||
)
|
||||
|
||||
Mod('pars',
|
||||
cls = 'frappy_psi.ultrasound.Pars',
|
||||
description = 'SEA parameters',
|
||||
)
|
||||
|
||||
def roi(nr, time=None, size=300):
|
||||
Mod(f'roi{nr}',
|
||||
cls = 'frappy_psi.ultrasound.Roi',
|
||||
description = f'I/Q of region {nr}',
|
||||
main = 'f',
|
||||
time=time or 4000,
|
||||
size=size,
|
||||
enable=time is not None,
|
||||
)
|
||||
|
||||
roi(0, 2450) # you may add size as argument if not default
|
||||
roi(1, 5950)
|
||||
roi(2, 9475)
|
||||
roi(3, 12900)
|
||||
roi(4, 16100)
|
||||
roi(5) # disabled
|
||||
roi(6)
|
||||
roi(7)
|
||||
roi(8)
|
||||
roi(9)
|
87
cfg/PEUS_cfg.py
Normal file
87
cfg/PEUS_cfg.py
Normal file
@ -0,0 +1,87 @@
|
||||
Node('PEUS.psi.ch',
|
||||
'ultrasound, pulse_echo configuration',
|
||||
interface='5000',
|
||||
)
|
||||
|
||||
Mod('u',
|
||||
'frappy_psi.ultrasound.PulseEcho',
|
||||
'ultrasound acquisition loop',
|
||||
freq='f',
|
||||
# pollinterval=0.1,
|
||||
time=900.0,
|
||||
size=5000.0,
|
||||
nr=500,
|
||||
sr=32768,
|
||||
bw=1e7,
|
||||
)
|
||||
|
||||
Mod('fio',
|
||||
'frappy_psi.ultrasound.FreqStringIO', '',
|
||||
uri='serial:///dev/ttyS1?baudrate=57600',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
'frappy_psi.ultrasound.Frequency',
|
||||
'writable for frequency',
|
||||
output='R', # L for LF (bnc), R for RF (type N)
|
||||
io='fio',
|
||||
amp=0.5, # VPP
|
||||
)
|
||||
|
||||
Mod('fdif',
|
||||
'frappy_psi.ultrasound.FrequencyDif',
|
||||
'writable for frequency minus base frequency',
|
||||
freq='f',
|
||||
base=41490200.0,
|
||||
)
|
||||
|
||||
# Mod('curves',
|
||||
# 'frappy_psi.ultrasound.Curves',
|
||||
# 't, I, Q and pulse arrays for plot',
|
||||
# )
|
||||
|
||||
def roi(name, time, size, components='iqpa', enable=True, control=False, freq=None, **kwds):
|
||||
description = 'I/Q of region {name}'
|
||||
if freq:
|
||||
kwds.update(cls='frappy_psi.ultrasound.ControlRoi',
|
||||
description=f'{description} as control loop',
|
||||
freq=freq, **kwds)
|
||||
else:
|
||||
kwds.update(cls='frappy_psi.ultrasound.Roi',
|
||||
description=description, **kwds)
|
||||
kwds.update({c: name + c for c in components})
|
||||
Mod(name,
|
||||
main='u',
|
||||
time=time,
|
||||
size=size,
|
||||
enable=enable,
|
||||
**kwds,
|
||||
)
|
||||
for c in components:
|
||||
Mod(name + c,
|
||||
'frappy.modules.Readable',
|
||||
f'{name}{c} component',
|
||||
)
|
||||
|
||||
# control loop
|
||||
roi('roi0', 2450, 300, freq='f', maxstep=100000, minstep=4000)
|
||||
# other rois
|
||||
roi('roi1', 5950, 300)
|
||||
roi('roi2', 9475, 300)
|
||||
roi('roi3', 12900, 300)
|
||||
#roi('roi4', 400, 30, False)
|
||||
#roi('roi5', 400, 30, False)
|
||||
#roi('roi6', 400, 30, False)
|
||||
#roi('roi7', 400, 30, False)
|
||||
#roi('roi8', 400, 30, False)
|
||||
#roi('roi9', 400, 30, False)
|
||||
|
||||
Mod('delay',
|
||||
'frappy_psi.dg645.Delay',
|
||||
'delay line with 2 channels',
|
||||
uri='serial:///dev/ttyS2',
|
||||
on1=1e-09,
|
||||
on2=1e-09,
|
||||
off1=4e-07,
|
||||
off2=6e-07,
|
||||
)
|
62
cfg/RUS.py
62
cfg/RUS.py
@ -1,62 +0,0 @@
|
||||
Node(equipment_id = 'r_ultrasound.psi.ch',
|
||||
description = 'resonant ultra sound setup',
|
||||
interface = 'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
cls = 'frappy_psi.ultrasound.Frequency',
|
||||
description = 'ultrasound frequency and acquisition loop',
|
||||
uri = 'serial:///dev/ttyS1',
|
||||
pars = 'pars',
|
||||
pollinterval = 0.1,
|
||||
time = 900, # start time
|
||||
size = 5000,
|
||||
freq = 1.e+03,
|
||||
basefreq = 1.E+3,
|
||||
control = False,
|
||||
rusmode = False,
|
||||
amp = 2.5,
|
||||
nr = 1, #500 #300 #100 #50 #30 #10 #5 #3 #1 #1000 #500 #300 #100 #50 #30 #10 #5 #3 #1 #500
|
||||
sr = 1E8, #16384
|
||||
plot = True,
|
||||
maxstep = 100000,
|
||||
bw = 10E6, #butter worth filter bandwidth
|
||||
maxy = 0.7, # y scale for plot
|
||||
curves = 'curves', # module to transmit curves:
|
||||
)
|
||||
|
||||
Mod('curves',
|
||||
cls = 'frappy_psi.ultrasound.Curves',
|
||||
description = 't, I, Q and pulse arrays for plot',
|
||||
)
|
||||
|
||||
Mod('roi0',
|
||||
cls = 'frappy_psi.ultrasound.Roi',
|
||||
description = 'I/Q of region in the control loop',
|
||||
time = 300, # this is the center of roi:
|
||||
size = 5000,
|
||||
main = f,
|
||||
)
|
||||
|
||||
Mod('roi1',
|
||||
cls = 'frappy_psi.ultrasound.Roi',
|
||||
description = 'I/Q of region 1',
|
||||
time = 100, # this is the center of roi:
|
||||
size = 300,
|
||||
main = f,
|
||||
)
|
||||
|
||||
Mod('delay',
|
||||
cls = 'frappy__psi.dg645.Delay',
|
||||
description = 'delay line with 2 channels',
|
||||
uri = 'serial:///dev/ttyS2',
|
||||
on1 = 1e-9,
|
||||
on2 = 1E-9,
|
||||
off1 = 400e-9,
|
||||
off2 = 600e-9,
|
||||
)
|
||||
|
||||
Mod('pars',
|
||||
cls = 'frappy_psi.ultrasound.Pars',
|
||||
description = 'SEA parameters',
|
||||
)
|
39
cfg/RUS_cfg.py
Normal file
39
cfg/RUS_cfg.py
Normal file
@ -0,0 +1,39 @@
|
||||
Node(equipment_id = 'r_ultrasound.psi.ch',
|
||||
description = 'resonant ultra sound setup',
|
||||
interface = 'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('iq',
|
||||
cls = 'frappy_psi.ultrasound.RUS',
|
||||
description = 'ultrasound iq mesurement',
|
||||
imod = 'i',
|
||||
qmod = 'q',
|
||||
freq='f',
|
||||
input_range=10, # VPP
|
||||
input_delay = 0,
|
||||
periods = 163,
|
||||
)
|
||||
|
||||
Mod('freqio',
|
||||
'frappy_psi.ultrasound.FreqStringIO',
|
||||
' ',
|
||||
uri = 'serial:///dev/ttyS1?baudrate=57600',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
cls = 'frappy_psi.ultrasound.Frequency',
|
||||
description = 'ultrasound frequency',
|
||||
io='freqio',
|
||||
output='L', # L for LF (bnc), R for RF (type N)
|
||||
target=10000,
|
||||
)
|
||||
|
||||
Mod('i',
|
||||
cls='frappy.modules.Readable',
|
||||
description='I component',
|
||||
)
|
||||
|
||||
Mod('q',
|
||||
cls='frappy.modules.Readable',
|
||||
description='Q component',
|
||||
)
|
15
cfg/addons/ah2700_cfg.py
Normal file → Executable file
15
cfg/addons/ah2700_cfg.py
Normal file → Executable file
@ -2,8 +2,21 @@ Node('ah2700.frappy.psi.ch',
|
||||
'Andeen Hagerlin 2700 Capacitance Bridge',
|
||||
)
|
||||
|
||||
Mod('cap_io',
|
||||
'frappy_psi.ah2700.Ah2700IO',
|
||||
'',
|
||||
uri='linse-976d-ts:3006',
|
||||
)
|
||||
|
||||
Mod('cap',
|
||||
'frappy_psi.ah2700.Capacitance',
|
||||
'capacitance',
|
||||
uri='dil4-ts.psi.ch:3008',
|
||||
io = 'cap_io',
|
||||
)
|
||||
|
||||
Mod('loss',
|
||||
'frappy_psi.parmod.Par',
|
||||
'loss parameter',
|
||||
read='cap.loss',
|
||||
unit='deg',
|
||||
)
|
||||
|
28
cfg/addons/sr830_cfg.py
Normal file
28
cfg/addons/sr830_cfg.py
Normal file
@ -0,0 +1,28 @@
|
||||
Node('srs830.ppms.psi.ch',
|
||||
'',
|
||||
interface='tcp://5000',
|
||||
)
|
||||
Mod('b',
|
||||
'frappy_psi.SR830.XY',
|
||||
'signal from Stanford Rasearch lockin',
|
||||
uri='linse-976d-ts:3002',
|
||||
)
|
||||
Mod('bx',
|
||||
'frappy_psi.parmod.Comp',
|
||||
'x-comp',
|
||||
read='b.value[0]',
|
||||
unit='V',
|
||||
)
|
||||
Mod('by',
|
||||
'frappy_psi.parmod.Comp',
|
||||
'y-comp',
|
||||
read='b.value[1]',
|
||||
unit='V',
|
||||
)
|
||||
Mod('bf',
|
||||
'frappy_psi.parmod.Par',
|
||||
'lockin frequency',
|
||||
read='b.freq',
|
||||
unit='Hz',
|
||||
)
|
||||
|
317
cfg/dil5_statemachine_cfg.py
Normal file
317
cfg/dil5_statemachine_cfg.py
Normal file
@ -0,0 +1,317 @@
|
||||
|
||||
Node('LOGO.psi.ch',
|
||||
'LOGO',
|
||||
interface='tcp://5010',
|
||||
secondary = ['ws://8010']
|
||||
)
|
||||
|
||||
Mod('io',
|
||||
'frappy_psi.logo.IO',
|
||||
'',
|
||||
ip_address = "192.168.0.3",
|
||||
tcap_client = 0x3000,
|
||||
tsap_server = 0x2000
|
||||
)
|
||||
|
||||
Mod('V1',
|
||||
'frappy_psi.logo.Valve',
|
||||
'Valves',
|
||||
io = 'io',
|
||||
vm_address_input ="V1025.0",
|
||||
vm_address_output ="V1064.3"
|
||||
)
|
||||
|
||||
Mod('V2',
|
||||
'frappy_psi.logo.Valve',
|
||||
'Valves',
|
||||
io = 'io',
|
||||
vm_address_input ="V1024.2",
|
||||
vm_address_output ="V1064.5"
|
||||
)
|
||||
|
||||
Mod('V4',
|
||||
'frappy_psi.logo.Valve',
|
||||
'Valves',
|
||||
io = 'io',
|
||||
vm_address_input ="V1024.5",
|
||||
vm_address_output ="V1064.5"
|
||||
)
|
||||
|
||||
Mod('V5',
|
||||
'frappy_psi.logo.Valve',
|
||||
'Valves',
|
||||
io = 'io',
|
||||
vm_address_input ="V1024.4",
|
||||
vm_address_output ="V1064.2"
|
||||
)
|
||||
|
||||
Mod('V9',
|
||||
'frappy_psi.logo.Valve',
|
||||
'Valves',
|
||||
io = 'io',
|
||||
vm_address_input ="V1024.3",
|
||||
vm_address_output ="V404.1"
|
||||
)
|
||||
|
||||
Mod('pump',
|
||||
'frappy_psi.logo.FluidMachines',
|
||||
'Pump',
|
||||
io = 'io',
|
||||
vm_address_output ="V414.1"
|
||||
)
|
||||
|
||||
Mod('compressor',
|
||||
'frappy_psi.logo.FluidMachines',
|
||||
'Compressor',
|
||||
io = 'io',
|
||||
vm_address_output ="V400.1"
|
||||
)
|
||||
|
||||
Mod('p2',
|
||||
'frappy_psi.logo.Pressure',
|
||||
'Pressure in mBar',
|
||||
io = 'io',
|
||||
vm_address ="VW0",
|
||||
)
|
||||
|
||||
Mod('p1',
|
||||
'frappy_psi.logo.Pressure',
|
||||
'Pressure in mBar',
|
||||
io = 'io',
|
||||
vm_address ="VW2",
|
||||
)
|
||||
|
||||
Mod('p5',
|
||||
'frappy_psi.logo.Pressure',
|
||||
'Pressure in mBar',
|
||||
io = 'io',
|
||||
vm_address ="VW4",
|
||||
)
|
||||
|
||||
Mod('Druckluft',
|
||||
'frappy_psi.logo.Airpressure',
|
||||
'Airpressure state',
|
||||
io = 'io',
|
||||
vm_address ="VW6",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Mod('SF1',
|
||||
'frappy_psi.logo.safetyfeatureState',
|
||||
'Safety Feature',
|
||||
io = 'io',
|
||||
vm_address ="V410.1",
|
||||
)
|
||||
|
||||
Mod('SF2',
|
||||
'frappy_psi.logo.safetyfeatureState',
|
||||
'Safety Feature',
|
||||
io = 'io',
|
||||
vm_address ="V406.1",
|
||||
)
|
||||
|
||||
Mod('SF3',
|
||||
'frappy_psi.logo.safetyfeatureState',
|
||||
'Safety Feature',
|
||||
io = 'io',
|
||||
vm_address ="V408.1",
|
||||
)
|
||||
|
||||
Mod('SF4',
|
||||
'frappy_psi.logo.safetyfeatureState',
|
||||
'Safety Feature',
|
||||
io = 'io',
|
||||
vm_address ="V412.1",
|
||||
)
|
||||
|
||||
Mod('p2max',
|
||||
'frappy_psi.logo.safetyfeatureParam',
|
||||
'Safety Feature Param',
|
||||
io = 'io',
|
||||
vm_address ="VW8",
|
||||
)
|
||||
|
||||
Mod('pcond',
|
||||
'frappy_psi.logo.safetyfeatureParam',
|
||||
'Safety Feature Param',
|
||||
io = 'io',
|
||||
vm_address ="VW10",
|
||||
)
|
||||
|
||||
Mod('p5min',
|
||||
'frappy_psi.logo.safetyfeatureParam',
|
||||
'Safety Feature Param',
|
||||
io = 'io',
|
||||
vm_address ="VW12",
|
||||
)
|
||||
|
||||
Mod('p5max',
|
||||
'frappy_psi.logo.safetyfeatureParam',
|
||||
'Safety Feature Param',
|
||||
io = 'io',
|
||||
vm_address ="VW14",
|
||||
)
|
||||
|
||||
"""
|
||||
Mod('io_ls273',
|
||||
'frappy_psi.ls372.StringIO',
|
||||
'io for Ls372',
|
||||
uri = 'localhost:2089',
|
||||
)
|
||||
Mod('sw',
|
||||
'frappy_psi.ls372.Switcher',
|
||||
'channel switcher',
|
||||
io = 'io_ls273',
|
||||
)
|
||||
Mod('res1',
|
||||
'frappy_psi.ls372.ResChannel',
|
||||
'resistivity chan 1',
|
||||
vexc = '2mV',
|
||||
channel = 1,
|
||||
switcher = 'sw',
|
||||
)
|
||||
"""
|
||||
|
||||
Mod('io_pfeiffer',
|
||||
'frappy_psi.pfeiffer_new.PfeifferProtocol',
|
||||
'',
|
||||
uri='serial:///dev/ttyUSB0?baudrate=9600+parity=none+bytesize=8+stopbits=1',
|
||||
)
|
||||
|
||||
Mod('io_turbo',
|
||||
'frappy_psi.pfeiffer_new.PfeifferProtocol',
|
||||
'',
|
||||
uri='serial:///dev/ttyUSB1?baudrate=9600+parity=none+bytesize=8+stopbits=1',
|
||||
)
|
||||
|
||||
Mod('p3',
|
||||
'frappy_psi.pfeiffer_new.RPT200',
|
||||
'Pressure in HPa',
|
||||
io = 'io_pfeiffer',
|
||||
address= 2,
|
||||
)
|
||||
|
||||
Mod('p4',
|
||||
'frappy_psi.pfeiffer_new.RPT200',
|
||||
'Pressure in HPa',
|
||||
io = 'io_pfeiffer',
|
||||
address= 4
|
||||
)
|
||||
|
||||
Mod('turbopump',
|
||||
'frappy_psi.pfeiffer_new.TCP400',
|
||||
'Pfeiffer Turbopump',
|
||||
io = 'io_turbo',
|
||||
address= 1
|
||||
)
|
||||
|
||||
Mod('MV10',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV10'
|
||||
)
|
||||
|
||||
Mod('MV13',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV13'
|
||||
)
|
||||
|
||||
Mod('MV8',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV8'
|
||||
)
|
||||
|
||||
Mod('MVB',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MVB'
|
||||
)
|
||||
|
||||
Mod('MV2',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV2'
|
||||
)
|
||||
|
||||
Mod('MV1',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV1'
|
||||
)
|
||||
|
||||
|
||||
Mod('MV3a',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV3a'
|
||||
)
|
||||
|
||||
Mod('MV3b',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV3b'
|
||||
)
|
||||
|
||||
Mod('GV1',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve GV1'
|
||||
)
|
||||
|
||||
Mod('GV2',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve GV2'
|
||||
)
|
||||
|
||||
Mod('MV14',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV14'
|
||||
)
|
||||
|
||||
Mod('MV12',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV12'
|
||||
)
|
||||
|
||||
Mod('MV11',
|
||||
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV11'
|
||||
)
|
||||
|
||||
Mod('MV9',
|
||||
'frappy_psi.manual_valves.ManualValve',
|
||||
'Manual Valve MV9'
|
||||
)
|
||||
|
||||
Mod('stateMachine',
|
||||
'frappy_psi.dilution_statemachine.DIL5',
|
||||
'Statemachine',
|
||||
|
||||
condenseline_pressure = "p2",
|
||||
condense_valve = "V9",
|
||||
dump_valve = "V4",
|
||||
circulate_pump = "pump",
|
||||
compressor = "compressor",
|
||||
turbopump = "turbopump",
|
||||
condenseline_valve = "V1",
|
||||
circuitshort_valve = "V2",
|
||||
still_pressure = "p3",
|
||||
#ls372 = "res1",
|
||||
V5 = "V5",
|
||||
p1 = "p1",
|
||||
|
||||
MV10 = 'MV10',
|
||||
MV13 ='MV13',
|
||||
MV8 = 'MV8',
|
||||
MVB = 'MVB',
|
||||
MV2 = 'MV2',
|
||||
|
||||
MV1 = 'MV1',
|
||||
MV3a = 'MV3a',
|
||||
MV3b = 'MV3b',
|
||||
GV1 = 'GV1',
|
||||
MV14 = 'MV14',
|
||||
MV12 = 'MV12',
|
||||
MV11 = 'MV11',
|
||||
MV9 = 'MV9',
|
||||
GV2 = 'GV2',
|
||||
condensing_p_low = 150,
|
||||
condensing_p_high = 250
|
||||
)
|
||||
|
||||
|
136
cfg/dummy_cfg.py
Normal file
136
cfg/dummy_cfg.py
Normal file
@ -0,0 +1,136 @@
|
||||
Node('test.config.frappy.demo',
|
||||
'''short description of the testing sec-node
|
||||
|
||||
This description for the node can be as long as you need if you use a multiline string.
|
||||
|
||||
Very long!
|
||||
The needed fields are Equipment id (1st argument), description (this)
|
||||
and the main interface of the node (3rd arg)
|
||||
''',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('attachtest',
|
||||
'frappy_demo.test.WithAtt',
|
||||
'test attached',
|
||||
att = 'LN2',
|
||||
)
|
||||
|
||||
Mod('pinata',
|
||||
'frappy_demo.test.Pin',
|
||||
'scan test',
|
||||
)
|
||||
|
||||
Mod('recursive',
|
||||
'frappy_demo.test.RecPin',
|
||||
'scan test',
|
||||
)
|
||||
|
||||
Mod('LN2',
|
||||
'frappy_demo.test.LN2',
|
||||
'random value between 0..100%',
|
||||
value = Param(default = 0, unit = '%'),
|
||||
)
|
||||
|
||||
Mod('heater',
|
||||
'frappy_demo.test.Heater',
|
||||
'some heater',
|
||||
maxheaterpower = 10,
|
||||
)
|
||||
|
||||
Mod('T1',
|
||||
'frappy_demo.test.Temp',
|
||||
'some temperature',
|
||||
sensor = 'X34598T7',
|
||||
)
|
||||
|
||||
Mod('T2',
|
||||
'frappy_demo.test.Temp',
|
||||
'some temperature',
|
||||
sensor = 'X34598T8',
|
||||
)
|
||||
|
||||
Mod('T3',
|
||||
'frappy_demo.test.Temp',
|
||||
'some temperature',
|
||||
sensor = 'X34598T9',
|
||||
)
|
||||
|
||||
Mod('Lower',
|
||||
'frappy_demo.test.Lower',
|
||||
'something else',
|
||||
)
|
||||
|
||||
Mod('Decision',
|
||||
'frappy_demo.test.Mapped',
|
||||
'Random value from configured property choices. Config accepts anything ' \
|
||||
'that can be converted to a list',
|
||||
choices = ['Yes', 'Maybe', 'No'],
|
||||
)
|
||||
|
||||
Mod('c',
|
||||
'frappy_demo.test.Commands',
|
||||
'a command test',
|
||||
)
|
||||
|
||||
Mod('cryo',
|
||||
'frappy_demo.cryo.Cryostat',
|
||||
'A simulated cc cryostat with heat-load, specific heat for the sample and a '
|
||||
'temperature dependent heat-link between sample and regulation.',
|
||||
group='very important/stuff',
|
||||
jitter=0.1,
|
||||
T_start=10.0,
|
||||
target=10.0,
|
||||
looptime=1,
|
||||
ramp=6,
|
||||
maxpower=20.0,
|
||||
heater=4.1,
|
||||
mode='pid',
|
||||
tolerance=0.1,
|
||||
window=30,
|
||||
timeout=900,
|
||||
p = Param(40, unit='%/K'), # in case 'default' is the first arg, we can omit 'default='
|
||||
i = 10,
|
||||
d = 2,
|
||||
pid = Group('p', 'i', 'd'),
|
||||
pollinterval = Param(export=False),
|
||||
value = Param(unit = 'K', test = 'customized value'),
|
||||
)
|
||||
|
||||
Mod('heatswitch',
|
||||
'frappy_demo.modules.Switch',
|
||||
'Heatswitch for `mf` device',
|
||||
switch_on_time = 5,
|
||||
switch_off_time = 10,
|
||||
)
|
||||
|
||||
Mod('bool',
|
||||
'frappy_demo.modules.BoolWritable',
|
||||
'boolean writable test',
|
||||
)
|
||||
|
||||
Mod('lscom',
|
||||
'frappy_psi.ls370sim.Ls370Sim',
|
||||
'simulated serial communicator to a LS 370',
|
||||
visibility = 3
|
||||
)
|
||||
|
||||
Mod('sw',
|
||||
'frappy_psi.ls370res.Switcher',
|
||||
'channel switcher for Lsc controller',
|
||||
io = 'lscom',
|
||||
)
|
||||
|
||||
Mod('a',
|
||||
'frappy_psi.ls370res.ResChannel',
|
||||
'resistivity',
|
||||
channel = 1,
|
||||
switcher = 'sw',
|
||||
)
|
||||
|
||||
Mod('b',
|
||||
'frappy_psi.ls370res.ResChannel',
|
||||
'resistivity',
|
||||
channel = 3,
|
||||
switcher = 'sw',
|
||||
)
|
100
cfg/fi2_cfg.py
Normal file
100
cfg/fi2_cfg.py
Normal file
@ -0,0 +1,100 @@
|
||||
Node('fi2.psi.ch',
|
||||
'vacuum furnace ILL Type',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('htr_io',
|
||||
'frappy_psi.tdkpower.IO',
|
||||
'powersupply communicator',
|
||||
uri = 'serial:///dev/ttyUSB0',
|
||||
)
|
||||
|
||||
Mod('htr',
|
||||
'frappy_psi.tdkpower.Power',
|
||||
'heater power',
|
||||
io= 'htr_io',
|
||||
)
|
||||
|
||||
Mod('out',
|
||||
'frappy_psi.tdkpower.Output',
|
||||
'heater output',
|
||||
io = 'htr_io',
|
||||
maxvolt = 5,
|
||||
maxcurrent = 25,
|
||||
)
|
||||
|
||||
Mod('relais',
|
||||
'frappy_psi.ionopimax.DigitalOutput',
|
||||
'relais for power output',
|
||||
addr = 'o2',
|
||||
)
|
||||
|
||||
Mod('T_main',
|
||||
'frappy_psi.ionopimax.CurrentInput',
|
||||
'sample temperature',
|
||||
addr = 'ai4',
|
||||
valuerange = (0, 1372),
|
||||
value = Param(unit='degC'),
|
||||
)
|
||||
|
||||
|
||||
Mod('T_extra',
|
||||
'frappy_psi.ionopimax.CurrentInput',
|
||||
'extra temperature',
|
||||
addr = 'ai3',
|
||||
valuerange = (0, 1372),
|
||||
value = Param(unit='degC'),
|
||||
|
||||
)
|
||||
|
||||
Mod('T_htr',
|
||||
'frappy_psi.ionopimax.CurrentInput',
|
||||
'heater temperature',
|
||||
addr = 'ai2',
|
||||
valuerange = (0, 1372),
|
||||
value = Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T_wall',
|
||||
'frappy_psi.ionopimax.VoltageInput',
|
||||
'furnace wall temperature',
|
||||
addr = 'av2',
|
||||
rawrange = (0, 1.5),
|
||||
valuerange = (0, 150),
|
||||
value = Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T',
|
||||
'frappy_psi.picontrol.PI',
|
||||
'controlled Temperature',
|
||||
input = 'T_htr',
|
||||
output = 'out',
|
||||
relais = 'relais',
|
||||
p = 2,
|
||||
i = 0.01,
|
||||
)
|
||||
|
||||
Mod('interlocks',
|
||||
'frappy_psi.furnace.Interlocks',
|
||||
'interlock parameters',
|
||||
input = 'T_htr',
|
||||
wall_T = 'T_wall',
|
||||
vacuum = 'p',
|
||||
relais = 'relais',
|
||||
control = 'T',
|
||||
wall_limit = 50,
|
||||
vacuum_limit = 0.1,
|
||||
)
|
||||
|
||||
Mod('p_io',
|
||||
'frappy_psi.pfeiffer.IO',
|
||||
'pressure io',
|
||||
uri='serial:///dev/ttyUSBlower',
|
||||
)
|
||||
|
||||
Mod('p',
|
||||
'frappy_psi.pfeiffer.Pressure',
|
||||
'pressure reading',
|
||||
io = 'p_io',
|
||||
)
|
||||
|
117
cfg/fi_cfg.py
Normal file
117
cfg/fi_cfg.py
Normal file
@ -0,0 +1,117 @@
|
||||
Node('fi.psi.ch',
|
||||
'ILL furnace',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('T_main',
|
||||
'frappy_psi.furnace.PRtransmitter',
|
||||
'sample temperature',
|
||||
addr='ai2',
|
||||
valuerange=(0, 2300),
|
||||
value=Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T_extra',
|
||||
'frappy_psi.furnace.PRtransmitter',
|
||||
'extra temperature',
|
||||
addr='ai1',
|
||||
valuerange=(0, 2300),
|
||||
value=Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T_wall',
|
||||
'frappy_psi.ionopimax.VoltageInput',
|
||||
'furnace wall temperature',
|
||||
addr='av2',
|
||||
rawrange=(0, 1.5),
|
||||
valuerange=(0, 150),
|
||||
value=Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T3',
|
||||
'frappy_psi.furnace.PRtransmitter',
|
||||
'extra temperature',
|
||||
addr='ai3',
|
||||
valuerange=(0, 1372),
|
||||
value=Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T4',
|
||||
'frappy_psi.furnace.PRtransmitter',
|
||||
'extra temperature',
|
||||
addr='ai4',
|
||||
valuerange=(0, 1372),
|
||||
value=Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('T',
|
||||
'frappy_psi.picontrol.PI',
|
||||
'controlled Temperature',
|
||||
input_module='T_main',
|
||||
output_module='htr',
|
||||
value = Param(unit='degC'),
|
||||
output_min = 0,
|
||||
output_max = 100,
|
||||
# relais='relais',
|
||||
p=0.1,
|
||||
i=0.01,
|
||||
)
|
||||
|
||||
Mod('htr_io',
|
||||
'frappy_psi.tdkpower.IO',
|
||||
'powersupply communicator',
|
||||
uri='serial:///dev/ttyUSB0?baudrate=9600',
|
||||
)
|
||||
|
||||
Mod('htr_power',
|
||||
'frappy_psi.tdkpower.Power',
|
||||
'heater power',
|
||||
io='htr_io',
|
||||
)
|
||||
|
||||
Mod('htr',
|
||||
'frappy_psi.furnace.TdkOutput',
|
||||
'heater output',
|
||||
io='htr_io',
|
||||
maxvolt=8,
|
||||
maxcurrent=200,
|
||||
)
|
||||
|
||||
Mod('flowswitch',
|
||||
'frappy_psi.ionopimax.DigitalInput',
|
||||
'flow switch',
|
||||
addr='dt2',
|
||||
true_level='low',
|
||||
)
|
||||
|
||||
Mod('interlocks',
|
||||
'frappy_psi.furnace.Interlocks',
|
||||
'interlock parameters',
|
||||
main_T='T_main',
|
||||
extra_T='T_extra',
|
||||
wall_T='T_wall',
|
||||
vacuum='p',
|
||||
control='T',
|
||||
htr='htr',
|
||||
flowswitch='flowswitch',
|
||||
wall_limit=50,
|
||||
main_T_limit = 1400,
|
||||
extra_T_limit = 1400,
|
||||
vacuum_limit=0.01,
|
||||
)
|
||||
|
||||
Mod('p',
|
||||
'frappy_psi.furnace.PKRgauge',
|
||||
'pressure reading',
|
||||
addr = 'av1',
|
||||
rawrange = (1.82, 8.6),
|
||||
valuerange = (5e-9, 1000),
|
||||
value = Param(unit='mbar'),
|
||||
)
|
||||
|
||||
Mod('vso',
|
||||
'frappy_psi.ionopimax.VoltagePower',
|
||||
'voltage power output',
|
||||
target = 24,
|
||||
export = False,
|
||||
)
|
130
cfg/fs_cfg.py
Normal file
130
cfg/fs_cfg.py
Normal file
@ -0,0 +1,130 @@
|
||||
Node('fs.psi.ch',
|
||||
'small vacuum furnace',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('T',
|
||||
'frappy_psi.picontrol.PI2',
|
||||
'controlled Temperature on sample (2nd loop)',
|
||||
input = 'T_sample',
|
||||
output = 'T_reg',
|
||||
relais = 'relais',
|
||||
p = 1.2,
|
||||
i = 0.005,
|
||||
)
|
||||
|
||||
Mod('T_reg',
|
||||
'frappy_psi.picontrol.PI',
|
||||
'controlled Temperature on heater',
|
||||
input = 'T_htr',
|
||||
output = 't_out',
|
||||
relais = 'relais',
|
||||
p = 1,
|
||||
i = 0.003,
|
||||
)
|
||||
|
||||
Mod('p_reg',
|
||||
'frappy_psi.picontrol.PI',
|
||||
'controlled pressure',
|
||||
input = 'p',
|
||||
output = 'p_out',
|
||||
relais = 'relais',
|
||||
p = 1,
|
||||
i = 0.005,
|
||||
)
|
||||
|
||||
Mod('T_htr',
|
||||
'frappy_psi.ionopimax.CurrentInput',
|
||||
'heater temperature',
|
||||
addr = 'ai4',
|
||||
valuerange = (0, 1372),
|
||||
value = Param(unit='degC'),
|
||||
|
||||
)
|
||||
|
||||
|
||||
Mod('T_sample',
|
||||
'frappy_psi.ionopimax.CurrentInput',
|
||||
'sample temperature',
|
||||
addr = 'ai3',
|
||||
valuerange = (0, 1372),
|
||||
value = Param(unit='degC'),
|
||||
|
||||
)
|
||||
|
||||
Mod('T_extra',
|
||||
'frappy_psi.ionopimax.CurrentInput',
|
||||
'extra temperature',
|
||||
addr = 'ai2',
|
||||
valuerange = (0, 1372),
|
||||
value = Param(unit='degC'),
|
||||
|
||||
)
|
||||
|
||||
|
||||
Mod('T_wall',
|
||||
'frappy_psi.ionopimax.VoltageInput',
|
||||
'furnace wall temperature',
|
||||
addr = 'av2',
|
||||
rawrange = (0, 1.5),
|
||||
valuerange = (0, 150),
|
||||
value = Param(unit='degC'),
|
||||
)
|
||||
|
||||
Mod('htr_io',
|
||||
'frappy_psi.bkpower.IO',
|
||||
'powersupply communicator',
|
||||
uri = 'serial:///dev/ttyUSBupper',
|
||||
)
|
||||
|
||||
Mod('htr',
|
||||
'frappy_psi.bkpower.Power',
|
||||
'heater power',
|
||||
io= 'htr_io',
|
||||
)
|
||||
|
||||
Mod('t_out',
|
||||
'frappy_psi.bkpower.Output',
|
||||
'heater output',
|
||||
p_value = 'p_out',
|
||||
io = 'htr_io',
|
||||
maxvolt = 50,
|
||||
maxcurrent = 2,
|
||||
)
|
||||
|
||||
Mod('relais',
|
||||
'frappy_psi.ionopimax.DigitalOutput',
|
||||
'relais for power output',
|
||||
addr = 'o2',
|
||||
)
|
||||
|
||||
Mod('interlocks',
|
||||
'frappy_psi.furnace.Interlocks',
|
||||
'interlock parameters',
|
||||
input = 'T_htr',
|
||||
wall_T = 'T_wall',
|
||||
htr_T = 'T_htr',
|
||||
main_T = 'T_sample',
|
||||
extra_T = 'T_extra',
|
||||
vacuum = 'p',
|
||||
relais = 'relais',
|
||||
control = 'T',
|
||||
wall_limit = 100,
|
||||
vacuum_limit = 0.1,
|
||||
)
|
||||
|
||||
Mod('p',
|
||||
'frappy_psi.ionopimax.LogVoltageInput',
|
||||
'pressure reading',
|
||||
addr = 'av1',
|
||||
rawrange = (1.82, 8.6),
|
||||
valuerange = (5e-9, 1000),
|
||||
value = Param(unit='mbar'),
|
||||
)
|
||||
|
||||
Mod('vso',
|
||||
'frappy_psi.ionopimax.VoltagePower',
|
||||
'voltage power output',
|
||||
target = 24,
|
||||
export = False,
|
||||
)
|
@ -4,33 +4,22 @@ Node('ls340test.psi.ch',
|
||||
)
|
||||
|
||||
Mod('io',
|
||||
'frappy_psi.lakeshore.Ls340IO',
|
||||
'frappy_psi.lakeshore.IO340',
|
||||
'communication to ls340',
|
||||
uri='tcp://ldmprep56-ts:3002'
|
||||
uri='tcp://localhost:7777'
|
||||
)
|
||||
|
||||
Mod('dev',
|
||||
'frappy_psi.lakeshore.Device340',
|
||||
'device for calcurve',
|
||||
io='io',
|
||||
curve_handling=True,
|
||||
)
|
||||
Mod('T',
|
||||
'frappy_psi.lakeshore.TemperatureLoop340',
|
||||
'sample temperature',
|
||||
output_module='Heater',
|
||||
target=Param(max=470),
|
||||
io='io',
|
||||
channel='B'
|
||||
)
|
||||
|
||||
Mod('T_cold_finger',
|
||||
'frappy_psi.lakeshore.Sensor340',
|
||||
'cold finger temperature',
|
||||
io='io',
|
||||
channel='A'
|
||||
)
|
||||
|
||||
Mod('Heater',
|
||||
'frappy_psi.lakeshore.HeaterOutput',
|
||||
'heater output',
|
||||
channel='B',
|
||||
io='io',
|
||||
resistance=25,
|
||||
max_power=50,
|
||||
current=1
|
||||
'sample temperature',
|
||||
# output_module='Heater',
|
||||
device='dev',
|
||||
channel='A',
|
||||
calcurve='x29746',
|
||||
)
|
||||
|
@ -6,7 +6,8 @@ Node('LscSIM.psi.ch',
|
||||
Mod('io',
|
||||
'frappy_psi.ls370res.StringIO',
|
||||
'io for Ls370',
|
||||
uri = 'localhost:2089',
|
||||
# uri = 'localhost:2089',
|
||||
uri = 'linse-976d-ts:3007',
|
||||
)
|
||||
Mod('sw',
|
||||
'frappy_psi.ls370res.Switcher',
|
||||
@ -17,7 +18,7 @@ Mod('res1',
|
||||
'frappy_psi.ls370res.ResChannel',
|
||||
'resistivity chan 1',
|
||||
vexc = '2mV',
|
||||
channel = 1,
|
||||
channel = 2,
|
||||
switcher = 'sw',
|
||||
)
|
||||
Mod('res2',
|
||||
|
@ -14,7 +14,7 @@ Mod('tt',
|
||||
io='sea_main',
|
||||
meaning=['temperature_regulation', 27],
|
||||
sea_object='tt',
|
||||
rel_paths=['.', 'tm', 'set', 'dblctrl'],
|
||||
rel_paths=['tm', '.', 'set', 'dblctrl'],
|
||||
)
|
||||
|
||||
Mod('cc',
|
||||
|
17
cfg/main/ori7test_cfg.py
Normal file
17
cfg/main/ori7test_cfg.py
Normal file
@ -0,0 +1,17 @@
|
||||
from frappy_psi.ccracks import Rack
|
||||
|
||||
Node('ori7test.psi.ch',
|
||||
'ORI7 test',
|
||||
'tcp://5000'
|
||||
)
|
||||
|
||||
rack = Rack(Mod)
|
||||
|
||||
rack.lakeshore()
|
||||
rack.sensor('Ts', channel='C', calcurve='x186350')
|
||||
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
|
||||
rack.heater('htr', 1, '100W', 25)
|
||||
|
||||
rack.he()
|
||||
rack.n2()
|
||||
rack.flow(min_open_pulse=0.03)
|
@ -170,20 +170,18 @@ Mod('htr_nvd',
|
||||
|
||||
# Motor controller is not yet available!
|
||||
#
|
||||
'''
|
||||
Mod('om_io',
|
||||
'frappy_psi.phytron.PhytronIO',
|
||||
'dom motor IO',
|
||||
uri='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=-180,
|
||||
target_max=360,
|
||||
encoder_mode='NO',
|
||||
target=Param(min=-180, max=360)
|
||||
)
|
||||
'''
|
||||
#Mod('om',
|
||||
# 'frappy_psi.phytron.Motor',
|
||||
# 'stick rotation, typically used for omega',
|
||||
# io='om_io',
|
||||
# target_min=-180,
|
||||
# target_max=360,
|
||||
# encoder_mode='NO',
|
||||
# target=Param(min=-180, max=360)
|
||||
#)
|
||||
|
@ -292,7 +292,7 @@
|
||||
{"path": "V3A", "type": "int", "readonly": false, "cmd": "dil V3A", "visibility": 3},
|
||||
{"path": "Roots", "type": "int", "readonly": false, "cmd": "dil Roots", "visibility": 3},
|
||||
{"path": "Aux", "type": "int", "readonly": false, "cmd": "dil Aux", "visibility": 3},
|
||||
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3"},
|
||||
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3", "visibility": 3},
|
||||
{"path": "closedelay", "type": "float", "readonly": false, "cmd": "dil closedelay", "visibility": 3},
|
||||
{"path": "extVersion", "type": "int", "readonly": false, "cmd": "dil extVersion", "visibility": 3},
|
||||
{"path": "pumpoff", "type": "int"},
|
||||
|
@ -292,7 +292,7 @@
|
||||
{"path": "V3A", "type": "int", "readonly": false, "cmd": "dil V3A", "visibility": 3},
|
||||
{"path": "Roots", "type": "int", "readonly": false, "cmd": "dil Roots", "visibility": 3},
|
||||
{"path": "Aux", "type": "int", "readonly": false, "cmd": "dil Aux", "visibility": 3},
|
||||
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3"},
|
||||
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3", "visibility": 3},
|
||||
{"path": "closedelay", "type": "float", "readonly": false, "cmd": "dil closedelay", "visibility": 3},
|
||||
{"path": "extVersion", "type": "int", "readonly": false, "cmd": "dil extVersion", "visibility": 3},
|
||||
{"path": "pumpoff", "type": "int"},
|
||||
|
@ -292,7 +292,7 @@
|
||||
{"path": "V3A", "type": "int", "readonly": false, "cmd": "dil V3A", "visibility": 3},
|
||||
{"path": "Roots", "type": "int", "readonly": false, "cmd": "dil Roots", "visibility": 3},
|
||||
{"path": "Aux", "type": "int", "readonly": false, "cmd": "dil Aux", "visibility": 3},
|
||||
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3"},
|
||||
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3", "visibility": 3},
|
||||
{"path": "closedelay", "type": "float", "readonly": false, "cmd": "dil closedelay", "visibility": 3},
|
||||
{"path": "extVersion", "type": "int", "readonly": false, "cmd": "dil extVersion", "visibility": 3},
|
||||
{"path": "pumpoff", "type": "int"},
|
||||
|
@ -88,16 +88,13 @@ Mod('interlocks',
|
||||
vacuum_limit = 0.1,
|
||||
)
|
||||
|
||||
Mod('p_io',
|
||||
'frappy_psi.pfeiffer.IO',
|
||||
'pressure io',
|
||||
uri='serial:///dev/ttyUSBlower',
|
||||
)
|
||||
|
||||
Mod('p',
|
||||
'frappy_psi.pfeiffer.Pressure',
|
||||
'frappy_psi.ionopimax.LogVoltageInput',
|
||||
'pressure reading',
|
||||
io = 'p_io',
|
||||
addr = 'av1',
|
||||
rawrange = (1.8, 8.6),
|
||||
valuerange = (1e-7, 1000),
|
||||
value = Param(unit='mbar'),
|
||||
)
|
||||
|
||||
|
||||
|
100
debian/changelog
vendored
100
debian/changelog
vendored
@ -1,4 +1,4 @@
|
||||
frappy-core (0.20.4) jammy; urgency=medium
|
||||
frappy-core (0.20.4) stable; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* remove unused file
|
||||
@ -17,7 +17,7 @@ frappy-core (0.20.4) jammy; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 14 Nov 2024 14:43:54 +0100
|
||||
|
||||
frappy-core (0.20.3) jammy; urgency=medium
|
||||
frappy-core (0.20.3) stable; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* fixup test for cfg_editor utils to run from non-checkout, and fix names, and remove example code
|
||||
@ -27,7 +27,7 @@ frappy-core (0.20.3) jammy; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 07 Nov 2024 10:57:11 +0100
|
||||
|
||||
frappy-core (0.20.2) jammy; urgency=medium
|
||||
frappy-core (0.20.2) stable; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* pylint: do not try to infer too much
|
||||
@ -73,7 +73,7 @@ frappy-core (0.20.2) jammy; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Wed, 06 Nov 2024 10:40:26 +0100
|
||||
|
||||
frappy-core (0.20.1) jammy; urgency=medium
|
||||
frappy-core (0.20.1) stable; urgency=medium
|
||||
|
||||
* gui: do not add a console logger when there is no sys.stdout
|
||||
* remove unused test class
|
||||
@ -83,7 +83,7 @@ frappy-core (0.20.1) jammy; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 17 Oct 2024 16:31:27 +0200
|
||||
|
||||
frappy-core (0.20.0) jammy; urgency=medium
|
||||
frappy-core (0.20.0) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* bin: remove make_doc
|
||||
@ -128,7 +128,7 @@ frappy-core (0.20.0) jammy; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Thu, 17 Oct 2024 14:24:29 +0200
|
||||
|
||||
frappy-core (0.19.10) jammy; urgency=medium
|
||||
frappy-core (0.19.10) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* debian: let frappy-core replace frappy-demo
|
||||
@ -138,25 +138,25 @@ frappy-core (0.19.10) jammy; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 07 Aug 2024 17:00:06 +0200
|
||||
|
||||
frappy-core (0.19.9) jammy; urgency=medium
|
||||
frappy-core (0.19.9) stable; urgency=medium
|
||||
|
||||
* debian: fix missing install dir
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 06 Aug 2024 16:02:50 +0200
|
||||
|
||||
frappy-core (0.19.8) jammy; urgency=medium
|
||||
frappy-core (0.19.8) stable; urgency=medium
|
||||
|
||||
* debian: move demo into core
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 06 Aug 2024 15:58:20 +0200
|
||||
|
||||
frappy-core (0.19.7) jammy; urgency=medium
|
||||
frappy-core (0.19.7) stable; urgency=medium
|
||||
|
||||
* lib: GeneralConfig fix missing keys logic
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 06 Aug 2024 15:04:07 +0200
|
||||
|
||||
frappy-core (0.19.6) jammy; urgency=medium
|
||||
frappy-core (0.19.6) stable; urgency=medium
|
||||
|
||||
[ Jens Krüger ]
|
||||
* SINQ/SEA: Fix import error due to None value
|
||||
@ -170,7 +170,7 @@ frappy-core (0.19.6) jammy; urgency=medium
|
||||
|
||||
-- Jens Krüger <jenkins@frm2.tum.de> Tue, 06 Aug 2024 13:56:51 +0200
|
||||
|
||||
frappy-core (0.19.5) jammy; urgency=medium
|
||||
frappy-core (0.19.5) stable; urgency=medium
|
||||
|
||||
* client: fix how to raise error on wrong ident
|
||||
* add missing requirements to setup.py
|
||||
@ -179,13 +179,13 @@ frappy-core (0.19.5) jammy; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Mon, 05 Aug 2024 09:30:53 +0200
|
||||
|
||||
frappy-core (0.19.4) jammy; urgency=medium
|
||||
frappy-core (0.19.4) stable; urgency=medium
|
||||
|
||||
* actually exclude cfg-editor
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Fri, 26 Jul 2024 11:46:10 +0200
|
||||
|
||||
frappy-core (0.19.3) jammy; urgency=medium
|
||||
frappy-core (0.19.3) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* frappy_psi.extparams.StructParam: fix doc + simplify
|
||||
@ -205,7 +205,7 @@ frappy-core (0.19.3) jammy; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Fri, 26 Jul 2024 08:36:43 +0200
|
||||
|
||||
frappy-core (0.19.2) jammy; urgency=medium
|
||||
frappy-core (0.19.2) stable; urgency=medium
|
||||
|
||||
[ l_samenv ]
|
||||
* fix missing update after error on parameter
|
||||
@ -230,7 +230,7 @@ frappy-core (0.19.2) jammy; urgency=medium
|
||||
|
||||
-- l_samenv <jenkins@frm2.tum.de> Tue, 18 Jun 2024 15:21:43 +0200
|
||||
|
||||
frappy-core (0.19.1) jammy; urgency=medium
|
||||
frappy-core (0.19.1) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* SecopClient.online must be True while activating
|
||||
@ -242,7 +242,7 @@ frappy-core (0.19.1) jammy; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Fri, 07 Jun 2024 16:50:33 +0200
|
||||
|
||||
frappy-core (0.19.0) jammy; urgency=medium
|
||||
frappy-core (0.19.0) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* simulation: extra_params might be a list
|
||||
@ -298,14 +298,14 @@ frappy-core (0.19.0) jammy; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Thu, 16 May 2024 11:31:25 +0200
|
||||
|
||||
frappy-core (0.18.1) focal; urgency=medium
|
||||
frappy-core (0.18.1) stable; urgency=medium
|
||||
|
||||
* mlz: Zapf fix unit handling and small errors
|
||||
* mlz: entangle fix limit check
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 24 Jan 2024 14:59:21 +0100
|
||||
|
||||
frappy-core (0.18.0) focal; urgency=medium
|
||||
frappy-core (0.18.0) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Add shutdownModule function
|
||||
@ -416,7 +416,7 @@ frappy-core (0.18.0) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 17 Jan 2024 12:35:00 +0100
|
||||
|
||||
frappy-core (0.17.13) focal; urgency=medium
|
||||
frappy-core (0.17.13) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* add egg-info to gitignore
|
||||
@ -437,7 +437,7 @@ frappy-core (0.17.13) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 20 Jun 2023 14:38:00 +0200
|
||||
|
||||
frappy-core (0.17.12) focal; urgency=medium
|
||||
frappy-core (0.17.12) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Warn about duplicate module definitions in a file
|
||||
@ -462,7 +462,7 @@ frappy-core (0.17.12) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 13 Jun 2023 06:51:27 +0200
|
||||
|
||||
frappy-core (0.17.11) focal; urgency=medium
|
||||
frappy-core (0.17.11) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Add __format__ to EnumMember
|
||||
@ -535,7 +535,7 @@ frappy-core (0.17.11) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Thu, 25 May 2023 09:38:24 +0200
|
||||
|
||||
frappy-core (0.17.10) focal; urgency=medium
|
||||
frappy-core (0.17.10) stable; urgency=medium
|
||||
|
||||
* Change leftover %-logging calls to lazy
|
||||
* Convert formatting automatically to f-strings
|
||||
@ -547,25 +547,25 @@ frappy-core (0.17.10) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 19 Apr 2023 14:32:52 +0200
|
||||
|
||||
frappy-core (0.17.9) focal; urgency=medium
|
||||
frappy-core (0.17.9) stable; urgency=medium
|
||||
|
||||
* interactive client: avoid messing up the input line
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Tue, 11 Apr 2023 16:09:03 +0200
|
||||
|
||||
frappy-core (0.17.8) focal; urgency=medium
|
||||
frappy-core (0.17.8) stable; urgency=medium
|
||||
|
||||
* Debian: Fix typo
|
||||
|
||||
-- Jens Krüger <jenkins@frm2.tum.de> Wed, 05 Apr 2023 07:20:25 +0200
|
||||
|
||||
frappy-core (0.17.7) focal; urgency=medium
|
||||
frappy-core (0.17.7) stable; urgency=medium
|
||||
|
||||
* Debian: add pyqtgraph dependency
|
||||
|
||||
-- Jens Krüger <jenkins@frm2.tum.de> Wed, 05 Apr 2023 07:07:24 +0200
|
||||
|
||||
frappy-core (0.17.6) focal; urgency=medium
|
||||
frappy-core (0.17.6) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* gui: show parameter properties again
|
||||
@ -585,25 +585,25 @@ frappy-core (0.17.6) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 04 Apr 2023 08:42:26 +0200
|
||||
|
||||
frappy-core (0.17.5) focal; urgency=medium
|
||||
frappy-core (0.17.5) stable; urgency=medium
|
||||
|
||||
* Fix generator
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 22 Mar 2023 12:32:06 +0100
|
||||
|
||||
frappy-core (0.17.4) focal; urgency=medium
|
||||
frappy-core (0.17.4) stable; urgency=medium
|
||||
|
||||
* Fix entangle integration bugs
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 22 Mar 2023 11:44:34 +0100
|
||||
|
||||
frappy-core (0.17.3) focal; urgency=medium
|
||||
frappy-core (0.17.3) stable; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Mar 2023 15:55:09 +0100
|
||||
|
||||
frappy-core (0.17.2) focal; urgency=medium
|
||||
frappy-core (0.17.2) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Fix Simulation and Proxy
|
||||
@ -740,7 +740,7 @@ frappy-core (0.17.2) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Mar 2023 15:49:06 +0100
|
||||
|
||||
frappy-core (0.17.1) focal; urgency=medium
|
||||
frappy-core (0.17.1) stable; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* gitignore: ignore demo PID file
|
||||
@ -759,7 +759,7 @@ frappy-core (0.17.1) focal; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 17:44:56 +0100
|
||||
|
||||
frappy-core (0.17.0) focal; urgency=medium
|
||||
frappy-core (0.17.0) stable; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Rework GUI.
|
||||
@ -770,37 +770,37 @@ frappy-core (0.17.0) focal; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Feb 2023 13:52:17 +0100
|
||||
|
||||
frappy-core (0.16.1) focal; urgency=medium
|
||||
frappy-core (0.16.1) stable; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:44:28 +0100
|
||||
|
||||
frappy-core (0.16.4) focal; urgency=medium
|
||||
frappy-core (0.16.4) stable; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:09:20 +0100
|
||||
|
||||
frappy-core (0.16.3) focal; urgency=medium
|
||||
frappy-core (0.16.3) stable; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:00:15 +0100
|
||||
|
||||
frappy-core (0.16.2) focal; urgency=medium
|
||||
frappy-core (0.16.2) stable; urgency=medium
|
||||
|
||||
* gui: move icon resources for the cfg editor to its subdirectory
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 07:50:13 +0100
|
||||
|
||||
frappy-core (0.16.1) focal; urgency=medium
|
||||
frappy-core (0.16.1) stable; urgency=medium
|
||||
|
||||
* add frappy-cli to package
|
||||
|
||||
-- Enrico Faulhaber <jenkins@frm2.tum.de> Mon, 20 Feb 2023 17:17:23 +0100
|
||||
|
||||
frappy-core (0.16.0) focal; urgency=medium
|
||||
frappy-core (0.16.0) stable; urgency=medium
|
||||
|
||||
[ Enrico Faulhaber ]
|
||||
* fix sorce package name
|
||||
@ -862,7 +862,7 @@ frappy-core (0.16.0) focal; urgency=medium
|
||||
|
||||
-- Enrico Faulhaber <jenkins@frm2.tum.de> Mon, 20 Feb 2023 16:15:10 +0100
|
||||
|
||||
frappy-core (0.15.0) focal; urgency=medium
|
||||
frappy-core (0.15.0) stable; urgency=medium
|
||||
|
||||
[ Björn Pedersen ]
|
||||
* Remove iohandler left-overs from docs
|
||||
@ -892,7 +892,7 @@ frappy-core (0.15.0) focal; urgency=medium
|
||||
|
||||
-- Björn Pedersen <jenkins@frm2.tum.de> Thu, 10 Nov 2022 14:46:01 +0100
|
||||
|
||||
secop-core (0.14.3) focal; urgency=medium
|
||||
secop-core (0.14.3) stable; urgency=medium
|
||||
|
||||
[ Enrico Faulhaber ]
|
||||
* change repo to secop/frappy
|
||||
@ -908,13 +908,13 @@ secop-core (0.14.3) focal; urgency=medium
|
||||
|
||||
-- Enrico Faulhaber <jenkins@frm2.tum.de> Thu, 03 Nov 2022 13:51:52 +0100
|
||||
|
||||
secop-core (0.14.2) focal; urgency=medium
|
||||
secop-core (0.14.2) stable; urgency=medium
|
||||
|
||||
* systemd generator: adapt to changed config API
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 20 Oct 2022 15:38:45 +0200
|
||||
|
||||
secop-core (0.14.1) focal; urgency=medium
|
||||
secop-core (0.14.1) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* secop_psi.entangle.AnalogInput: fix main value
|
||||
@ -926,7 +926,7 @@ secop-core (0.14.1) focal; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Thu, 20 Oct 2022 14:04:07 +0200
|
||||
|
||||
secop-core (0.14.0) focal; urgency=medium
|
||||
secop-core (0.14.0) stable; urgency=medium
|
||||
|
||||
* add simple interactive python client
|
||||
* fix undefined status in softcal
|
||||
@ -940,7 +940,7 @@ secop-core (0.14.0) focal; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Wed, 19 Oct 2022 11:31:50 +0200
|
||||
|
||||
secop-core (0.13.1) focal; urgency=medium
|
||||
secop-core (0.13.1) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* an enum with value 0 should be interpreted as False
|
||||
@ -951,7 +951,7 @@ secop-core (0.13.1) focal; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 02 Aug 2022 15:31:52 +0200
|
||||
|
||||
secop-core (0.13.0) focal; urgency=medium
|
||||
secop-core (0.13.0) stable; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* debian: fix email addresses in changelog
|
||||
@ -1014,13 +1014,13 @@ secop-core (0.13.0) focal; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 02 Aug 2022 09:47:06 +0200
|
||||
|
||||
secop-core (0.12.4) focal; urgency=medium
|
||||
secop-core (0.12.4) stable; urgency=medium
|
||||
|
||||
* fix command inheritance
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Thu, 11 Nov 2021 16:21:19 +0100
|
||||
|
||||
secop-core (0.12.3) focal; urgency=medium
|
||||
secop-core (0.12.3) stable; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* Makefile: fix docker image
|
||||
@ -1043,7 +1043,7 @@ secop-core (0.12.3) focal; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@jenkins01.admin.frm2.tum.de> Wed, 10 Nov 2021 16:33:19 +0100
|
||||
|
||||
secop-core (0.12.2) focal; urgency=medium
|
||||
secop-core (0.12.2) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* fix issue with new syntax in simulation
|
||||
@ -1055,13 +1055,13 @@ secop-core (0.12.2) focal; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Tue, 18 May 2021 10:29:17 +0200
|
||||
|
||||
secop-core (0.12.1) focal; urgency=medium
|
||||
secop-core (0.12.1) stable; urgency=medium
|
||||
|
||||
* remove secop-console from debian *.install file
|
||||
|
||||
-- Enrico Faulhaber <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 09:42:53 +0200
|
||||
|
||||
secop-core (0.12.0) focal; urgency=medium
|
||||
secop-core (0.12.0) stable; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* make datatypes immutable
|
||||
|
1
debian/compat
vendored
1
debian/compat
vendored
@ -1 +0,0 @@
|
||||
11
|
4
debian/control
vendored
4
debian/control
vendored
@ -2,7 +2,7 @@ Source: frappy-core
|
||||
Section: contrib/misc
|
||||
Priority: optional
|
||||
Maintainer: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
Build-Depends: debhelper (>= 11~),
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
dh-python,
|
||||
python3 (>=3.6),
|
||||
python3-all,
|
||||
@ -20,7 +20,7 @@ Build-Depends: debhelper (>= 11~),
|
||||
git,
|
||||
markdown,
|
||||
python3-daemon
|
||||
Standards-Version: 4.1.4
|
||||
Standards-Version: 4.6.2
|
||||
X-Python3-Version: >= 3.6
|
||||
|
||||
Package: frappy-core
|
||||
|
@ -734,7 +734,7 @@ class SecopClient(ProxyClient):
|
||||
"""
|
||||
self.connect() # make sure we are connected
|
||||
datatype = self.modules[module]['parameters'][parameter]['datatype']
|
||||
value = datatype.from_string(formatted)
|
||||
value = datatype.export_value(datatype.from_string(formatted))
|
||||
self.request(WRITEREQUEST, self.identifier[module, parameter], value)
|
||||
return self.cache[module, parameter]
|
||||
|
||||
@ -753,6 +753,25 @@ class SecopClient(ProxyClient):
|
||||
data = datatype.import_value(data)
|
||||
return data, qualifiers
|
||||
|
||||
def execCommandFromString(self, module, command, formatted_argument):
|
||||
"""call command from string argument
|
||||
|
||||
return formatted data and qualifiers
|
||||
"""
|
||||
self.connect()
|
||||
datatype = self.modules[module]['commands'][command]['datatype'].argument
|
||||
if datatype:
|
||||
argument = datatype.from_string(formatted_argument)
|
||||
else:
|
||||
if formatted_argument:
|
||||
raise WrongTypeError('command has no argument')
|
||||
argument = None
|
||||
data, qualifiers = self.request(COMMANDREQUEST, self.identifier[module, command], argument)[2]
|
||||
datatype = self.modules[module]['commands'][command]['datatype'].result
|
||||
if datatype:
|
||||
data = datatype.format_value(data)
|
||||
return data, qualifiers
|
||||
|
||||
def updateValue(self, module, param, value, timestamp, readerror):
|
||||
datatype = self.modules[module]['parameters'][param]['datatype']
|
||||
if readerror:
|
||||
|
@ -498,7 +498,7 @@ class Console(code.InteractiveConsole):
|
||||
history = None
|
||||
if readline:
|
||||
try:
|
||||
history = expanduser(f'~/.frappy-{name}-history')
|
||||
history = expanduser(f'~/.config/frappy/{name}-history')
|
||||
readline.read_history_file(history)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
@ -538,10 +538,10 @@ def init(*nodes):
|
||||
return success
|
||||
|
||||
|
||||
def interact(usage_tail=''):
|
||||
def interact(usage_tail='', appname=None):
|
||||
empty = '_c0' not in clientenv.namespace
|
||||
print(USAGE.format(
|
||||
client_name='cli' if empty else '_c0',
|
||||
client_assign="\ncli = Client('localhost:5000')\n" if empty else '',
|
||||
tail=usage_tail))
|
||||
Console()
|
||||
Console(name=f'cli-{appname}' if appname else 'cli')
|
||||
|
@ -95,7 +95,9 @@ class Collector:
|
||||
self.cls = cls
|
||||
|
||||
def add(self, *args, **kwds):
|
||||
self.list.append(self.cls(*args, **kwds))
|
||||
result = self.cls(*args, **kwds)
|
||||
self.list.append(result)
|
||||
return result
|
||||
|
||||
def append(self, mod):
|
||||
self.list.append(mod)
|
||||
|
@ -27,7 +27,6 @@
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>18</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
|
@ -21,7 +21,6 @@
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
<underline>true</underline>
|
||||
</font>
|
||||
|
50
frappy/lib/units.py
Normal file
50
frappy/lib/units.py
Normal file
@ -0,0 +1,50 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""handling of prefixes of physical units"""
|
||||
|
||||
import re
|
||||
import prefixed
|
||||
|
||||
prefixed.SI_MAGNITUDE['u'] = 1e-6 # accept 'u' as replacement for 'µ'
|
||||
|
||||
|
||||
class NumberWithUnit:
|
||||
def __init__(self, *units):
|
||||
pfx = "|".join(prefixed.SI_MAGNITUDE)
|
||||
unt = "|".join(units)
|
||||
self.units = units
|
||||
self.pattern = re.compile(rf'\s*([+-]?\d*\.?\d*(?:[eE][+-]?\d+)?\s*(?:{pfx})?)({unt})\s*$')
|
||||
|
||||
def parse(self, value):
|
||||
"""parse and return number and value"""
|
||||
match = self.pattern.match(value)
|
||||
if not match:
|
||||
raise ValueError(f'{value!r} can not be interpreted as a number with unit {",".join(self.units)}')
|
||||
number, unit = match.groups()
|
||||
return prefixed.Float(number), unit
|
||||
|
||||
def getnum(self, value):
|
||||
"""parse and return value only"""
|
||||
return self.parse(value)[0]
|
||||
|
||||
|
||||
def format_with_unit(value, unit='', digits=3):
|
||||
return f'{prefixed.Float(value):.{digits}H}{unit}'
|
@ -60,7 +60,6 @@ class HasAccessibles(HasProperties):
|
||||
(so the dispatcher will get notified of changed values)
|
||||
"""
|
||||
isWrapped = False
|
||||
checkedMethods = set()
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls): # pylint: disable=too-many-branches
|
||||
@ -114,8 +113,8 @@ class HasAccessibles(HasProperties):
|
||||
wrapped_name = '_' + cls.__name__
|
||||
for pname, pobj in accessibles.items():
|
||||
# wrap of reading/writing funcs
|
||||
if not isinstance(pobj, Parameter):
|
||||
# nothing to do for Commands
|
||||
if not isinstance(pobj, Parameter) or pobj.optional:
|
||||
# nothing to do for Commands and optional parameters
|
||||
continue
|
||||
|
||||
rname = 'read_' + pname
|
||||
@ -199,16 +198,15 @@ class HasAccessibles(HasProperties):
|
||||
new_wfunc.__module__ = cls.__module__
|
||||
cls.wrappedAttributes[wname] = new_wfunc
|
||||
|
||||
cls.checkedMethods.update(cls.wrappedAttributes)
|
||||
|
||||
# check for programming errors
|
||||
for attrname in dir(cls):
|
||||
for attrname, func in cls.__dict__.items():
|
||||
prefix, _, pname = attrname.partition('_')
|
||||
if not pname:
|
||||
continue
|
||||
if prefix == 'do':
|
||||
raise ProgrammingError(f'{cls.__name__!r}: old style command {attrname!r} not supported anymore')
|
||||
if prefix in ('read', 'write') and attrname not in cls.checkedMethods:
|
||||
if (prefix in ('read', 'write') and attrname not in cls.wrappedAttributes
|
||||
and not hasattr(func, 'poll')): # may be a handler, which always has a poll attribute
|
||||
raise ProgrammingError(f'{cls.__name__}.{attrname} defined, but {pname!r} is no parameter')
|
||||
|
||||
try:
|
||||
@ -325,6 +323,7 @@ class Module(HasAccessibles):
|
||||
|
||||
pollInfo = None
|
||||
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
||||
__poller = None # the poller thread, if used
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the secnode for interacting with other modules and the
|
||||
@ -390,6 +389,8 @@ class Module(HasAccessibles):
|
||||
accessibles = self.accessibles
|
||||
self.accessibles = {}
|
||||
for aname, aobj in accessibles.items():
|
||||
if aobj.optional:
|
||||
continue
|
||||
# make a copy of the Parameter/Command object
|
||||
aobj = aobj.copy()
|
||||
acfg = cfgdict.pop(aname, None)
|
||||
@ -450,9 +451,12 @@ class Module(HasAccessibles):
|
||||
self.parameters[name] = accessible
|
||||
if isinstance(accessible, Command):
|
||||
self.commands[name] = accessible
|
||||
if cfg:
|
||||
if cfg is not None:
|
||||
try:
|
||||
for propname, propvalue in cfg.items():
|
||||
if propname in {'value', 'default', 'constant'}:
|
||||
# these properties have ValueType(), but should be checked for datatype
|
||||
accessible.datatype(cfg[propname])
|
||||
accessible.setProperty(propname, propvalue)
|
||||
except KeyError:
|
||||
self.errors.append(f"'{name}' has no property '{propname}'")
|
||||
@ -609,7 +613,7 @@ class Module(HasAccessibles):
|
||||
# we do not need self.errors any longer. should we delete it?
|
||||
# del self.errors
|
||||
if self.polledModules:
|
||||
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
|
||||
self.__poller = mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
|
||||
self.startModuleDone = True
|
||||
|
||||
def initialReads(self):
|
||||
@ -622,8 +626,28 @@ class Module(HasAccessibles):
|
||||
all parameters are polled once
|
||||
"""
|
||||
|
||||
def stopPollThread(self):
|
||||
"""trigger the poll thread to stop
|
||||
|
||||
this is called on shutdown
|
||||
"""
|
||||
if self.__poller:
|
||||
self.polledModules.clear()
|
||||
self.triggerPoll.set()
|
||||
|
||||
def joinPollThread(self, timeout):
|
||||
"""wait for poll thread to finish
|
||||
|
||||
if the wait time exceeds <timeout> seconds, return and log a warning
|
||||
"""
|
||||
if self.__poller:
|
||||
self.stopPollThread()
|
||||
self.__poller.join(timeout)
|
||||
if self.__poller.is_alive():
|
||||
self.log.warning('can not stop poller')
|
||||
|
||||
def shutdownModule(self):
|
||||
"""called when the sever shuts down
|
||||
"""called when the server shuts down
|
||||
|
||||
any cleanup-work should be performed here, like closing threads and
|
||||
saving data.
|
||||
@ -726,13 +750,14 @@ class Module(HasAccessibles):
|
||||
if not polled_modules: # no polls needed - exit thread
|
||||
return
|
||||
to_poll = ()
|
||||
while True:
|
||||
while modules: # modules will be cleared on shutdown
|
||||
now = time.time()
|
||||
wait_time = 999
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
|
||||
pinfo.last_slow + mobj.slowinterval - now)
|
||||
if pinfo:
|
||||
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
|
||||
pinfo.last_slow + mobj.slowinterval - now)
|
||||
if wait_time > 0 and not to_poll:
|
||||
# nothing to do
|
||||
self.triggerPoll.wait(wait_time)
|
||||
@ -741,7 +766,7 @@ class Module(HasAccessibles):
|
||||
# call doPoll of all modules where due
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if now > pinfo.last_main + pinfo.interval:
|
||||
if pinfo and now > pinfo.last_main + pinfo.interval:
|
||||
try:
|
||||
pinfo.last_main = (now // pinfo.interval) * pinfo.interval
|
||||
except ZeroDivisionError:
|
||||
@ -761,7 +786,7 @@ class Module(HasAccessibles):
|
||||
# collect due slow polls
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if now > pinfo.last_slow + mobj.slowinterval:
|
||||
if pinfo and now > pinfo.last_slow + mobj.slowinterval:
|
||||
to_poll.extend(pinfo.polled_parameters)
|
||||
pinfo.last_slow = (now // mobj.slowinterval) * mobj.slowinterval
|
||||
if to_poll:
|
||||
|
@ -68,8 +68,8 @@ class Writable(Readable):
|
||||
target_dt.compatible(value_dt)
|
||||
except Exception:
|
||||
if type(value_dt) == type(target_dt):
|
||||
raise ConfigError('the target range extends beyond the value range') from None
|
||||
raise ProgrammingError('the datatypes of target and value are not compatible') from None
|
||||
raise ConfigError(f'{name}: the target range extends beyond the value range') from None
|
||||
raise ProgrammingError(f'{name}: the datatypes of target and value are not compatible') from None
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
|
@ -47,6 +47,7 @@ class Accessible(HasProperties):
|
||||
"""
|
||||
|
||||
ownProperties = None
|
||||
optional = False
|
||||
|
||||
def init(self, kwds):
|
||||
# do not use self.propertyValues.update here, as no invalid values should be
|
||||
@ -96,6 +97,8 @@ class Accessible(HasProperties):
|
||||
props = []
|
||||
for k, v in sorted(self.propertyValues.items()):
|
||||
props.append(f'{k}={v!r}')
|
||||
if self.optional:
|
||||
props.append('optional=True')
|
||||
return f"{self.__class__.__name__}({', '.join(props)})"
|
||||
|
||||
def fixExport(self):
|
||||
@ -191,8 +194,9 @@ class Parameter(Accessible):
|
||||
readerror = None
|
||||
omit_unchanged_within = 0
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
||||
def __init__(self, description=None, datatype=None, inherit=True, optional=False, **kwds):
|
||||
super().__init__()
|
||||
self.optional = optional
|
||||
if 'poll' in kwds and generalConfig.tolerate_poll_property:
|
||||
kwds.pop('poll')
|
||||
if datatype is None:
|
||||
@ -226,10 +230,16 @@ class Parameter(Accessible):
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.parameters[self.name].value
|
||||
try:
|
||||
return instance.parameters[self.name].value
|
||||
except KeyError:
|
||||
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj.announceUpdate(self.name, value)
|
||||
try:
|
||||
obj.announceUpdate(self.name, value)
|
||||
except KeyError:
|
||||
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
@ -366,9 +376,6 @@ class Command(Accessible):
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
# optional = Property(
|
||||
# '[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||
# export=False, default=False, settable=False)
|
||||
datatype = Property(
|
||||
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||
DataTypeType(), extname='datainfo', export='always')
|
||||
@ -384,8 +391,9 @@ class Command(Accessible):
|
||||
|
||||
func = None
|
||||
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, optional=False, **kwds):
|
||||
super().__init__()
|
||||
self.optional = optional
|
||||
if 'datatype' in kwds:
|
||||
# self.init will complain about invalid keywords except 'datatype', as this is a property
|
||||
raise ProgrammingError("Command() got an invalid keyword 'datatype'")
|
||||
@ -411,8 +419,8 @@ class Command(Accessible):
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
if self.func is None:
|
||||
raise ProgrammingError(f'Command {owner.__name__}.{name} must be used as a method decorator')
|
||||
if self.func is None and not self.optional:
|
||||
raise ProgrammingError(f'Command {owner.__name__}.{name} must be optional or used as a method decorator')
|
||||
|
||||
self.fixExport()
|
||||
self.datatype = CommandType(self.argument, self.result)
|
||||
|
@ -131,14 +131,16 @@ class HasProperties(HasDescriptors):
|
||||
properties = {}
|
||||
# using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases
|
||||
for base in reversed(cls.__mro__):
|
||||
properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)})
|
||||
for key, value in base.__dict__.items():
|
||||
if isinstance(value, Property):
|
||||
properties[key] = value
|
||||
elif isinstance(value, HasProperties): # value is a Parameter. allow to override
|
||||
properties.pop(key, None)
|
||||
cls.propertyDict = properties
|
||||
# treat overriding properties with bare values
|
||||
for pn, po in list(properties.items()):
|
||||
value = getattr(cls, pn, po)
|
||||
if isinstance(value, HasProperties): # value is a Parameter, allow override
|
||||
properties.pop(pn)
|
||||
elif not isinstance(value, Property): # attribute may be a bare value
|
||||
if not isinstance(value, Property): # attribute may be a bare value
|
||||
po = po.copy()
|
||||
try:
|
||||
# try to apply bare value to Property
|
||||
|
@ -265,9 +265,9 @@ class Dispatcher:
|
||||
modulename, exportedname = specifier, None
|
||||
if ':' in specifier:
|
||||
modulename, exportedname = specifier.split(':', 1)
|
||||
if modulename not in self.secnode.export:
|
||||
raise NoSuchModuleError(f'Module {modulename!r} does not exist')
|
||||
moduleobj = self.secnode.get_module(modulename)
|
||||
if moduleobj is None or not moduleobj.export:
|
||||
raise NoSuchModuleError(f'Module {modulename!r} does not exist')
|
||||
if exportedname is not None:
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname, True)
|
||||
if pname and pname not in moduleobj.accessibles:
|
||||
@ -281,7 +281,7 @@ class Dispatcher:
|
||||
else:
|
||||
# activate all modules
|
||||
self._active_connections.add(conn)
|
||||
modules = [(m, None) for m in self.secnode.export]
|
||||
modules = [(m, None) for m in self.secnode.get_exported_modules()]
|
||||
|
||||
# send updates for all subscribed values.
|
||||
# note: The initial poll already happend before the server is active
|
||||
|
@ -33,7 +33,7 @@ from frappy.io import HasIO
|
||||
DISCONNECTED = Readable.Status.ERROR, 'disconnected'
|
||||
|
||||
|
||||
class ProxyModule(HasIO, Module):
|
||||
class Proxy(HasIO, Module):
|
||||
module = Property('remote module name', datatype=StringType(), default='')
|
||||
status = Parameter('connection status', Readable.status.datatype) # add status even when not a Readable
|
||||
|
||||
@ -42,6 +42,17 @@ class ProxyModule(HasIO, Module):
|
||||
_secnode = None
|
||||
enablePoll = False
|
||||
|
||||
def __new__(cls, name, logger, cfgdict, srv):
|
||||
"""create a Proxy class based on remote_class"""
|
||||
remote_class = cfgdict.pop('remote_class')
|
||||
if isinstance(remote_class, dict):
|
||||
remote_class = remote_class['value']
|
||||
if 'description' not in cfgdict:
|
||||
cfgdict['description'] = (f"remote module {cfgdict.get('module', name)} "
|
||||
f"on {cfgdict.get('io', {'value:': '?'})['value']}")
|
||||
proxycls = proxy_class(remote_class)
|
||||
return super().__new__(proxycls, name, logger, cfgdict, srv)
|
||||
|
||||
def ioClass(self, name, logger, opts, srv):
|
||||
opts['description'] = f"secnode {opts.get('module', name)} on {opts['uri']}"
|
||||
return SecNode(name, logger, opts, srv)
|
||||
@ -131,19 +142,19 @@ class ProxyModule(HasIO, Module):
|
||||
pass # skip
|
||||
|
||||
|
||||
class ProxyReadable(ProxyModule, Readable):
|
||||
class ProxyReadable(Proxy, Readable):
|
||||
pass
|
||||
|
||||
|
||||
class ProxyWritable(ProxyModule, Writable):
|
||||
class ProxyWritable(Proxy, Writable):
|
||||
pass
|
||||
|
||||
|
||||
class ProxyDrivable(ProxyModule, Drivable):
|
||||
class ProxyDrivable(Proxy, Drivable):
|
||||
pass
|
||||
|
||||
|
||||
PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
||||
PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, Proxy]
|
||||
|
||||
|
||||
class SecNode(Module):
|
||||
@ -169,7 +180,7 @@ def proxy_class(remote_class, name=None):
|
||||
"""create a proxy class based on the definition of remote class
|
||||
|
||||
remote class is <import path>.<class name> of a class used on the remote node
|
||||
if name is not given, 'Proxy' + <class name> is used
|
||||
if name is not given, <class name> is used
|
||||
"""
|
||||
if isinstance(remote_class, type) and issubclass(remote_class, Module):
|
||||
rcls = remote_class
|
||||
@ -229,18 +240,3 @@ def proxy_class(remote_class, name=None):
|
||||
raise ConfigError(f'do not now about {aobj!r} in {remote_class}.accessibles')
|
||||
|
||||
return type(name+"_", (proxycls,), attrs)
|
||||
|
||||
|
||||
def Proxy(name, logger, cfgdict, srv):
|
||||
"""create a Proxy object based on remote_class
|
||||
|
||||
title cased as it acts like a class
|
||||
"""
|
||||
remote_class = cfgdict.pop('remote_class')
|
||||
if isinstance(remote_class, dict):
|
||||
remote_class = remote_class['value']
|
||||
|
||||
if 'description' not in cfgdict:
|
||||
cfgdict['description'] = f"remote module {cfgdict.get('module', name)} on {cfgdict.get('io', {'value:': '?'})['value']}"
|
||||
|
||||
return proxy_class(remote_class)(name, logger, cfgdict, srv)
|
||||
|
@ -102,7 +102,6 @@ class Handler:
|
||||
"""create the wrapped read_* or write_* methods"""
|
||||
# at this point, this 'method_names' entry is no longer used -> delete
|
||||
self.method_names.discard((self.func.__module__, self.func.__qualname__))
|
||||
owner.checkedMethods.add(name)
|
||||
for key in self.keys:
|
||||
wrapped = self.wrap(key)
|
||||
method_name = self.prefix + key
|
||||
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import time
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
|
||||
@ -26,6 +27,7 @@ from frappy.dynamic import Pinata
|
||||
from frappy.errors import ConfigError, NoSuchModuleError, NoSuchParameterError
|
||||
from frappy.lib import get_class
|
||||
from frappy.version import get_version
|
||||
from frappy.modules import Module
|
||||
|
||||
|
||||
class SecNode:
|
||||
@ -42,8 +44,6 @@ class SecNode:
|
||||
self.nodeprops = {}
|
||||
# map ALL modulename -> moduleobj
|
||||
self.modules = {}
|
||||
# list of EXPORTED modules
|
||||
self.export = []
|
||||
self.log = logger
|
||||
self.srv = srv
|
||||
# set of modules that failed creation
|
||||
@ -130,6 +130,9 @@ class SecNode:
|
||||
# creation has failed already once, do not try again
|
||||
return None
|
||||
cls = classname
|
||||
if not issubclass(cls, Module):
|
||||
self.errors.append(f'{cls.__name__} is not a Module')
|
||||
return None
|
||||
except Exception as e:
|
||||
if str(e) == 'no such class':
|
||||
self.errors.append(f'{classname} not found')
|
||||
@ -188,60 +191,62 @@ class SecNode:
|
||||
modname, len(pinata_modules))
|
||||
todos.extend(pinata_modules)
|
||||
|
||||
def export_accessibles(self, modulename):
|
||||
self.log.debug('export_accessibles(%r)', modulename)
|
||||
if modulename in self.export:
|
||||
# omit export=False params!
|
||||
res = OrderedDict()
|
||||
for aobj in self.get_module(modulename).accessibles.values():
|
||||
if aobj.export:
|
||||
res[aobj.export] = aobj.for_export()
|
||||
self.log.debug('list accessibles for module %s -> %r',
|
||||
modulename, res)
|
||||
return res
|
||||
self.log.debug('-> module is not to be exported!')
|
||||
return OrderedDict()
|
||||
def export_accessibles(self, modobj):
|
||||
self.log.debug('export_accessibles(%r)', modobj.name)
|
||||
# omit export=False params!
|
||||
res = OrderedDict()
|
||||
for aobj in modobj.accessibles.values():
|
||||
if aobj.export:
|
||||
res[aobj.export] = aobj.for_export()
|
||||
self.log.debug('list accessibles for module %s -> %r',
|
||||
modobj.name, res)
|
||||
return res
|
||||
|
||||
def build_descriptive_data(self):
|
||||
modules = {}
|
||||
result = {'modules': modules}
|
||||
for modulename in self.modules:
|
||||
modobj = self.get_module(modulename)
|
||||
if not modobj.export:
|
||||
continue
|
||||
# some of these need rework !
|
||||
mod_desc = {'accessibles': self.export_accessibles(modobj)}
|
||||
mod_desc.update(modobj.exportProperties())
|
||||
mod_desc.pop('export', None)
|
||||
modules[modulename] = mod_desc
|
||||
result['equipment_id'] = self.equipment_id
|
||||
result['firmware'] = 'FRAPPY ' + get_version()
|
||||
result['description'] = self.nodeprops['description']
|
||||
for prop, propvalue in self.nodeprops.items():
|
||||
if prop.startswith('_'):
|
||||
result[prop] = propvalue
|
||||
self.descriptive_data = result
|
||||
|
||||
def get_descriptive_data(self, specifier):
|
||||
"""returns a python object which upon serialisation results in the
|
||||
descriptive data"""
|
||||
specifier = specifier or ''
|
||||
modules = {}
|
||||
result = {'modules': modules}
|
||||
for modulename in self.export:
|
||||
module = self.get_module(modulename)
|
||||
if not module.export:
|
||||
continue
|
||||
# some of these need rework !
|
||||
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
||||
mod_desc.update(module.exportProperties())
|
||||
mod_desc.pop('export', False)
|
||||
modules[modulename] = mod_desc
|
||||
modname, _, pname = specifier.partition(':')
|
||||
modules = self.descriptive_data['modules']
|
||||
if modname in modules: # extension to SECoP standard: description of a single module
|
||||
result = modules[modname]
|
||||
if pname in result['accessibles']: # extension to SECoP standard: description of a single accessible
|
||||
# command is also accepted
|
||||
result = result['accessibles'][pname]
|
||||
elif pname:
|
||||
return result['accessibles'][pname]
|
||||
if pname:
|
||||
raise NoSuchParameterError(f'Module {modname!r} '
|
||||
f'has no parameter {pname!r}')
|
||||
elif not modname or modname == '.':
|
||||
result['equipment_id'] = self.equipment_id
|
||||
result['firmware'] = 'FRAPPY ' + get_version()
|
||||
result['description'] = self.nodeprops['description']
|
||||
for prop, propvalue in self.nodeprops.items():
|
||||
if prop.startswith('_'):
|
||||
result[prop] = propvalue
|
||||
else:
|
||||
raise NoSuchModuleError(f'Module {modname!r} does not exist')
|
||||
return result
|
||||
return result
|
||||
if not modname or modname == '.':
|
||||
return self.descriptive_data
|
||||
raise NoSuchModuleError(f'Module {modname!r} does not exist')
|
||||
|
||||
def get_exported_modules(self):
|
||||
return [m for m, o in self.modules.items() if o.export]
|
||||
|
||||
def add_module(self, module, modulename):
|
||||
"""Adds a named module object to this SecNode."""
|
||||
self.modules[modulename] = module
|
||||
if module.export:
|
||||
self.export.append(modulename)
|
||||
|
||||
# def remove_module(self, modulename_or_obj):
|
||||
# moduleobj = self.get_module(modulename_or_obj)
|
||||
@ -255,6 +260,15 @@ class SecNode:
|
||||
|
||||
def shutdown_modules(self):
|
||||
"""Call 'shutdownModule' for all modules."""
|
||||
# stop pollers
|
||||
for mod in self.modules.values():
|
||||
mod.stopPollThread()
|
||||
# do not yet join here, as we want to wait in parallel
|
||||
now = time.time()
|
||||
deadline = now + 0.5 # should be long enough for most read functions to finish
|
||||
for mod in self.modules.values():
|
||||
mod.joinPollThread(max(0, deadline - now))
|
||||
now = time.time()
|
||||
for name in self._getSortedModules():
|
||||
self.modules[name].shutdownModule()
|
||||
|
||||
|
@ -289,7 +289,6 @@ class Server:
|
||||
If there are errors that occur, they will be collected and emitted
|
||||
together in the end.
|
||||
"""
|
||||
errors = []
|
||||
opts = dict(self.node_cfg)
|
||||
cls = get_class(opts.pop('cls'))
|
||||
self.secnode = SecNode(self.name, self.log.getChild('secnode'), opts, self)
|
||||
@ -301,10 +300,9 @@ class Server:
|
||||
self.secnode.add_secnode_property(k, opts.pop(k))
|
||||
|
||||
self.secnode.create_modules()
|
||||
# initialize all modules by getting them with Dispatcher.get_module,
|
||||
# which is done in the get_descriptive data
|
||||
# TODO: caching, to not make this extra work
|
||||
self.secnode.get_descriptive_data('')
|
||||
# initialize modules by calling self.secnode.get_module for all of them
|
||||
# this is done in build_descriptive_data even for unexported modules
|
||||
self.secnode.build_descriptive_data()
|
||||
# =========== All modules are initialized ===========
|
||||
|
||||
# all errors from initialization process
|
||||
|
@ -142,4 +142,5 @@ class SimDrivable(SimReadable, Drivable):
|
||||
|
||||
@Command
|
||||
def stop(self):
|
||||
"""set target to value"""
|
||||
self.target = self.value
|
||||
|
@ -215,7 +215,10 @@ class HasStates:
|
||||
self.read_status()
|
||||
if fast_poll:
|
||||
sm.reset_fast_poll = True
|
||||
self.setFastPoll(True)
|
||||
if fast_poll is True:
|
||||
self.setFastPoll(True)
|
||||
else:
|
||||
self.setFastPoll(True, fast_poll)
|
||||
self.pollInfo.trigger(True) # trigger poller
|
||||
|
||||
def stop_machine(self, stopped_status=(IDLE, 'stopped')):
|
||||
|
@ -161,7 +161,7 @@ class Cryostat(CryoBase):
|
||||
|
||||
by setting the current setpoint as new target"""
|
||||
# XXX: discussion: take setpoint or current value ???
|
||||
self.write_target(self.setpoint)
|
||||
self.write_target(self.setpoint if self.mode == 'ramp' else self.value)
|
||||
|
||||
#
|
||||
# calculation helpers
|
||||
|
@ -28,7 +28,7 @@ import time
|
||||
from frappy.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, IntRange, StringType, StructOf, TupleOf
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.modules import Drivable, Readable, Attached
|
||||
from frappy.modules import Drivable, Readable, Writable, Attached
|
||||
from frappy.modules import Parameter as SECoP_Parameter
|
||||
from frappy.properties import Property
|
||||
|
||||
@ -99,6 +99,14 @@ class Switch(Drivable):
|
||||
self.log.info(info)
|
||||
|
||||
|
||||
class BoolWritable(Writable):
|
||||
value = Parameter('boolean', BoolType())
|
||||
target = Parameter('boolean', BoolType())
|
||||
|
||||
def write_target(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
class MagneticField(Drivable):
|
||||
"""a liquid magnet
|
||||
"""
|
||||
|
@ -22,11 +22,13 @@
|
||||
|
||||
import random
|
||||
|
||||
from frappy.datatypes import FloatRange, StringType, ValueType, TupleOf, StructOf, ArrayOf
|
||||
from frappy.datatypes import FloatRange, StringType, ValueType, TupleOf, StructOf, ArrayOf, StatusType, BoolType
|
||||
from frappy.modules import Communicator, Drivable, Parameter, Property, Readable, Module, Attached
|
||||
from frappy.params import Command
|
||||
from frappy.dynamic import Pinata
|
||||
from frappy.errors import RangeError, HardwareError
|
||||
from frappy.core import IDLE, WARN, ERROR, DISABLED
|
||||
|
||||
|
||||
class Pin(Pinata):
|
||||
def scanModules(self):
|
||||
@ -105,13 +107,27 @@ class Temp(Drivable):
|
||||
readonly=False,
|
||||
unit='K',
|
||||
)
|
||||
enabled = Parameter('enable', BoolType(), default=True, readonly=False)
|
||||
status = Parameter(datatype=StatusType(Readable, 'DISABLED'))
|
||||
_status = IDLE, ''
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
value = round(100 * random.random(), 1)
|
||||
if value > 75:
|
||||
self._status = ERROR, 'sensor break'
|
||||
elif value > 50:
|
||||
self._status = WARN, 'out of calibrated range'
|
||||
else:
|
||||
self._status = IDLE, ''
|
||||
self.read_status()
|
||||
return value
|
||||
|
||||
def write_target(self, target):
|
||||
pass
|
||||
|
||||
def read_status(self):
|
||||
return self._status if self.enabled else (DISABLED, 'disabled')
|
||||
|
||||
|
||||
class Lower(Communicator):
|
||||
"""Communicator returning a lowercase version of the request"""
|
||||
|
@ -1,313 +1,397 @@
|
||||
# *****************************************************************************
|
||||
# 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:
|
||||
# Damaris Tartarotti Maimone
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Wrapper for the ADQ data acquisition card for ultrasound"""
|
||||
import sys
|
||||
import atexit
|
||||
import signal
|
||||
import time
|
||||
import numpy as np
|
||||
import ctypes as ct
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
|
||||
# For different trigger modes
|
||||
SW_TRIG = 1
|
||||
# The following external trigger does not work if the level of the trigger is very close to 0.5V.
|
||||
# Now we have it close to 3V, and it works
|
||||
EXT_TRIG_1 = 2
|
||||
EXT_TRIG_2 = 7
|
||||
EXT_TRIG_3 = 8
|
||||
LVL_TRIG = 3
|
||||
INT_TRIG = 4
|
||||
LVL_FALLING = 0
|
||||
LVL_RISING = 1
|
||||
|
||||
ADQ_CLOCK_INT_INTREF = 0 # internal clock source
|
||||
ADQ_CLOCK_EXT_REF = 1 # internal clock source, external reference
|
||||
ADQ_CLOCK_EXT_CLOCK = 2 # External clock source
|
||||
|
||||
ADQ_TRANSFER_MODE_NORMAL = 0x00
|
||||
ADQ_CHANNELS_MASK = 0x3
|
||||
|
||||
GHz = 1e9
|
||||
|
||||
|
||||
class Adq:
|
||||
sample_rate = 2 * GHz
|
||||
max_number_of_channels = 2
|
||||
ndecimate = 50 # decimation ratio (2GHz / 40 MHz)
|
||||
number_of_records = 1
|
||||
samples_per_record = 16384
|
||||
bw_cutoff = 10E6
|
||||
trigger = EXT_TRIG_1
|
||||
adq_num = 1
|
||||
UNDEFINED = -1
|
||||
IDLE = 0
|
||||
BUSY = 1
|
||||
READY = 2
|
||||
status = UNDEFINED
|
||||
data = None
|
||||
|
||||
def __init__(self):
|
||||
global ADQAPI
|
||||
ADQAPI = ct.cdll.LoadLibrary("libadq.so.0")
|
||||
|
||||
ADQAPI.ADQAPI_GetRevision()
|
||||
|
||||
# Manually set return type from some ADQAPI functions
|
||||
ADQAPI.CreateADQControlUnit.restype = ct.c_void_p
|
||||
ADQAPI.ADQ_GetRevision.restype = ct.c_void_p
|
||||
ADQAPI.ADQ_GetPtrStream.restype = ct.POINTER(ct.c_int16)
|
||||
ADQAPI.ADQControlUnit_FindDevices.argtypes = [ct.c_void_p]
|
||||
# Create ADQControlUnit
|
||||
self.adq_cu = ct.c_void_p(ADQAPI.CreateADQControlUnit())
|
||||
ADQAPI.ADQControlUnit_EnableErrorTrace(self.adq_cu, 3, '.')
|
||||
|
||||
# Find ADQ devices
|
||||
ADQAPI.ADQControlUnit_FindDevices(self.adq_cu)
|
||||
n_of_adq = ADQAPI.ADQControlUnit_NofADQ(self.adq_cu)
|
||||
if n_of_adq != 1:
|
||||
raise RuntimeError('number of ADQs must be 1, not %d' % n_of_adq)
|
||||
|
||||
rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num)
|
||||
revision = ct.cast(rev, ct.POINTER(ct.c_int))
|
||||
print('\nConnected to ADQ #1')
|
||||
# Print revision information
|
||||
print('FPGA Revision: {}'.format(revision[0]))
|
||||
if revision[1]:
|
||||
print('Local copy')
|
||||
else:
|
||||
print('SVN Managed')
|
||||
if revision[2]:
|
||||
print('Mixed Revision')
|
||||
else:
|
||||
print('SVN Updated')
|
||||
print('')
|
||||
|
||||
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF)
|
||||
|
||||
##########################
|
||||
# Test pattern
|
||||
# ADQAPI.ADQ_SetTestPatternMode(self.adq_cu, self.adq_num, 4)
|
||||
##########################
|
||||
# Sample skip
|
||||
# ADQAPI.ADQ_SetSampleSkip(self.adq_cu, self.adq_num, 1)
|
||||
##########################
|
||||
|
||||
# set trigger mode
|
||||
if not ADQAPI.ADQ_SetTriggerMode(self.adq_cu, self.adq_num, self.trigger):
|
||||
raise RuntimeError('ADQ_SetTriggerMode failed.')
|
||||
if self.trigger == LVL_TRIG:
|
||||
if not ADQAPI.ADQ_SetLvlTrigLevel(self.adq_cu, self.adq_num, -100):
|
||||
raise RuntimeError('ADQ_SetLvlTrigLevel failed.')
|
||||
if not ADQAPI.ADQ_SetTrigLevelResetValue(self.adq_cu, self.adq_num, 1000):
|
||||
raise RuntimeError('ADQ_SetTrigLevelResetValue failed.')
|
||||
if not ADQAPI.ADQ_SetLvlTrigChannel(self.adq_cu, self.adq_num, 1):
|
||||
raise RuntimeError('ADQ_SetLvlTrigChannel failed.')
|
||||
if not ADQAPI.ADQ_SetLvlTrigEdge(self.adq_cu, self.adq_num, LVL_RISING):
|
||||
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
|
||||
elif self.trigger == EXT_TRIG_1:
|
||||
if not ADQAPI.ADQ_SetExternTrigEdge(self.adq_cu, self.adq_num, 2):
|
||||
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
|
||||
# if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)):
|
||||
# raise RuntimeError('SetTriggerThresholdVoltage failed.')
|
||||
print("CHANNEL:"+str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
|
||||
atexit.register(self.deletecu)
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
|
||||
def init(self, samples_per_record=None, number_of_records=None):
|
||||
"""initialize dimensions"""
|
||||
if samples_per_record:
|
||||
self.samples_per_record = samples_per_record
|
||||
if number_of_records:
|
||||
self.number_of_records = number_of_records
|
||||
# Setup target buffers for data
|
||||
self.target_buffers = (ct.POINTER(ct.c_int16 * self.samples_per_record * self.number_of_records)
|
||||
* self.max_number_of_channels)()
|
||||
for bufp in self.target_buffers:
|
||||
bufp.contents = (ct.c_int16 * self.samples_per_record * self.number_of_records)()
|
||||
|
||||
def deletecu(self):
|
||||
# Only disarm trigger after data is collected
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_MultiRecordClose(self.adq_cu, self.adq_num)
|
||||
# Delete ADQControlunit
|
||||
ADQAPI.DeleteADQControlUnit(self.adq_cu)
|
||||
|
||||
def start(self):
|
||||
# Start acquisition
|
||||
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
|
||||
self.number_of_records,
|
||||
self.samples_per_record)
|
||||
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
|
||||
self.status = self.BUSY
|
||||
|
||||
def get_status(self):
|
||||
"""check if ADQ card is busy"""
|
||||
if self.status == self.BUSY:
|
||||
if ADQAPI.ADQ_GetAcquiredAll(self.adq_cu, self.adq_num):
|
||||
self.status = self.READY
|
||||
else:
|
||||
if self.trigger == SW_TRIG:
|
||||
ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num)
|
||||
return self.status
|
||||
|
||||
def get_data(self, dataclass, **kwds):
|
||||
"""when ready, get raw data from card, else return cached data
|
||||
|
||||
return
|
||||
"""
|
||||
if self.get_status() == self.READY:
|
||||
# Get data from ADQ
|
||||
if not ADQAPI.ADQ_GetData(self.adq_cu, self.adq_num, self.target_buffers,
|
||||
self.samples_per_record * self.number_of_records, 2,
|
||||
0, self.number_of_records, ADQ_CHANNELS_MASK,
|
||||
0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL):
|
||||
raise RuntimeError('no success from ADQ_GetDATA')
|
||||
self.data = dataclass(self, **kwds)
|
||||
self.status = self.IDLE
|
||||
if self.status == self.UNDEFINED:
|
||||
raise RuntimeError('no data available yet')
|
||||
return self.data
|
||||
|
||||
|
||||
class PEdata:
|
||||
def __init__(self, adq):
|
||||
self.sample_rate = adq.sample_rate
|
||||
self.samp_freq = self.sample_rate / GHz
|
||||
self.number_of_records = adq.number_of_records
|
||||
data = []
|
||||
for ch in range(2):
|
||||
onedim = np.frombuffer(adq.target_buffers[ch].contents, dtype=np.int16)
|
||||
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2**14)) # 14 bits ADC
|
||||
# Now this is an array with all records, but the time is artificial
|
||||
self.data = data
|
||||
|
||||
def sinW(self, sig, freq, ti, tf):
|
||||
# sig: signal array
|
||||
# freq
|
||||
# ti, tf: initial and end time
|
||||
si = int(ti * self.samp_freq)
|
||||
nperiods = freq * (tf - ti)
|
||||
n = int(round(max(2, int(nperiods)) / nperiods * (tf-ti) * self.samp_freq))
|
||||
self.nperiods = n
|
||||
t = np.arange(si, len(sig)) / self.samp_freq
|
||||
t = t[:n]
|
||||
self.pulselen = n / self.samp_freq
|
||||
sig = sig[si:si+n]
|
||||
a = 2*np.sum(sig*np.cos(2*np.pi*freq*t))/len(sig)
|
||||
b = 2*np.sum(sig*np.sin(2*np.pi*freq*t))/len(sig)
|
||||
return a, b
|
||||
|
||||
def mix(self, sigin, sigout, freq, ti, tf):
|
||||
# sigin, sigout: signal array, incomping, output
|
||||
# freq
|
||||
# ti, tf: initial and end time of sigin
|
||||
a, b = self.sinW(sigin, freq, ti, tf)
|
||||
amp = np.sqrt(a**2 + b**2)
|
||||
a, b = a/amp, b/amp
|
||||
# si = int(ti * self.samp_freq)
|
||||
t = np.arange(len(sigout)) / self.samp_freq
|
||||
wave1 = sigout * (a * np.cos(2*np.pi*freq*t) + b * np.sin(2*np.pi*freq*t))
|
||||
wave2 = sigout * (a * np.sin(2*np.pi*freq*t) - b * np.cos(2*np.pi*freq*t))
|
||||
return wave1, wave2
|
||||
|
||||
def averageiq(self, data, freq, ti, tf):
|
||||
"""Average over records"""
|
||||
iorq = np.array([self.mix(data[0][i], data[1][i], freq, ti, tf) for i in range(self.number_of_records)])
|
||||
return iorq.sum(axis=0) / self.number_of_records
|
||||
|
||||
def filtro(self, iorq, cutoff):
|
||||
# butter lowpass
|
||||
nyq = 0.5 * self.sample_rate
|
||||
normal_cutoff = cutoff / nyq
|
||||
order = 5
|
||||
b, a = butter(order, normal_cutoff, btype='low', analog=False)
|
||||
iqf = [filtfilt(b, a, iorq[i]) for i in np.arange(len(iorq))]
|
||||
return iqf
|
||||
|
||||
def box(self, iorq, ti, tf):
|
||||
si = int(self.samp_freq * ti)
|
||||
sf = int(self.samp_freq * tf)
|
||||
bxa = [sum(iorq[i][si:sf])/(sf-si) for i in np.arange(len(iorq))]
|
||||
return bxa
|
||||
|
||||
def gates_and_curves(self, freq, pulse, roi, bw_cutoff):
|
||||
"""return iq values of rois and prepare plottable curves for iq"""
|
||||
self.ndecimate = int(round(self.sample_rate / freq))
|
||||
# times = []
|
||||
# times.append(('aviq', time.time()))
|
||||
iq = self.averageiq(self.data, freq / GHz, *pulse)
|
||||
# times.append(('filtro', time.time()))
|
||||
iqf = self.filtro(iq, bw_cutoff)
|
||||
m = len(iqf[0]) // self.ndecimate
|
||||
ll = m * self.ndecimate
|
||||
iqf = [iqfx[0:ll] for iqfx in iqf]
|
||||
# times.append(('iqdec', time.time()))
|
||||
iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2)
|
||||
t_axis = np.arange(m) * self.ndecimate / self.samp_freq
|
||||
pulsig = np.abs(self.data[0][0])
|
||||
# times.append(('pulsig', time.time()))
|
||||
pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1)
|
||||
self.curves = (t_axis, iqd[0], iqd[1], pulsig)
|
||||
# print(times)
|
||||
return [self.box(iqf, *r) for r in roi]
|
||||
|
||||
|
||||
class RUSdata:
|
||||
def __init__(self, adq, freq, periods):
|
||||
self.sample_rate = adq.sample_rate
|
||||
self.freq = freq
|
||||
self.periods = periods
|
||||
self.samples_per_record = adq.samples_per_record
|
||||
input_signal = np.frombuffer(adq.target_buffers[0].contents, dtype=np.int16)
|
||||
output_signal = np.frombuffer(adq.target_buffers[1].contents, dtype=np.int16)
|
||||
complex_sinusoid = np.exp(1j * 2 * np.pi * self.freq / self.sample_rate * np.arange(len(input_signal)))
|
||||
self.input_mixed = input_signal * complex_sinusoid
|
||||
self.output_mixed = output_signal * complex_sinusoid
|
||||
self.input_mean = self.input_mixed.mean()
|
||||
self.output_mean = self.output_mixed.mean()
|
||||
self.iq = self.output_mean / self.input_mean
|
||||
|
||||
def get_reduced(self, mixed):
|
||||
"""get reduced array and normalize"""
|
||||
nper = self.samples_per_record // self.periods
|
||||
mean = mixed.mean()
|
||||
return mixed[:self.period * nper].reshape((-1, nper)).mean(axis=0) / mean
|
||||
|
||||
def calc_quality(self):
|
||||
"""get signal quality info
|
||||
|
||||
quality info (small values indicate good quality):
|
||||
- input_std and output_std:
|
||||
the imaginary part indicates deviations in phase
|
||||
the real part indicates deviations in amplitude
|
||||
- input_slope and output_slope:
|
||||
the imaginary part indicates a turning phase (rad/sec)
|
||||
the real part indicates changes in amplitude (0.01 ~= 1%/sec)
|
||||
"""
|
||||
reduced = self.get_reduced(self.input_mixed)
|
||||
self.input_stdev = reduced.std()
|
||||
|
||||
reduced = self.get_reduced(self.output_mixed)
|
||||
timeaxis = np.arange(len(reduced)) * self.sample_rate / self.freq
|
||||
self.output_slope = np.polyfit(timeaxis, reduced, 1)[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:
|
||||
# Damaris Tartarotti Maimone
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Wrapper for the ADQ data acquisition card for ultrasound"""
|
||||
import sys
|
||||
import atexit
|
||||
import signal
|
||||
import time
|
||||
import numpy as np
|
||||
import ctypes as ct
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
|
||||
# For different trigger modes
|
||||
SW_TRIG = 1
|
||||
# The following external trigger does not work if the level of the trigger is very close to 0.5V.
|
||||
# Now we have it close to 3V, and it works
|
||||
EXT_TRIG_1 = 2
|
||||
EXT_TRIG_2 = 7
|
||||
EXT_TRIG_3 = 8
|
||||
LVL_TRIG = 3
|
||||
INT_TRIG = 4
|
||||
LVL_FALLING = 0
|
||||
LVL_RISING = 1
|
||||
|
||||
ADQ_CLOCK_INT_INTREF = 0 # internal clock source
|
||||
ADQ_CLOCK_EXT_REF = 1 # internal clock source, external reference
|
||||
ADQ_CLOCK_EXT_CLOCK = 2 # External clock source
|
||||
|
||||
ADQ_TRANSFER_MODE_NORMAL = 0x00
|
||||
ADQ_CHANNELS_MASK = 0x3
|
||||
|
||||
GHz = 1e9
|
||||
RMS_TO_VPP = 2 * np.sqrt(2)
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self):
|
||||
self.data = [(time.time(), 'start')]
|
||||
|
||||
def __call__(self, text=''):
|
||||
now = time.time()
|
||||
prev = self.data[-1][0]
|
||||
self.data.append((now, text))
|
||||
return now - prev
|
||||
|
||||
def summary(self):
|
||||
return ' '.join(f'{txt} {tim:.3f}' for tim, txt in self.data[1:])
|
||||
|
||||
def show(self):
|
||||
first = prev = self.data[0][0]
|
||||
print('---', first)
|
||||
for tim, txt in self.data[1:]:
|
||||
print(f'{(tim - first) * 1000:9.3f} {(tim - prev) * 1000:9.3f} ms {txt}')
|
||||
prev = tim
|
||||
|
||||
|
||||
class Adq:
|
||||
sample_rate = 2 * GHz
|
||||
max_number_of_channels = 2
|
||||
ndecimate = 50 # decimation ratio (2GHz / 40 MHz)
|
||||
number_of_records = 1
|
||||
samples_per_record = 16384
|
||||
bw_cutoff = 10E6
|
||||
trigger = EXT_TRIG_1
|
||||
adq_num = 1
|
||||
data = None
|
||||
busy = False
|
||||
|
||||
def __init__(self):
|
||||
global ADQAPI
|
||||
ADQAPI = ct.cdll.LoadLibrary("libadq.so.0")
|
||||
|
||||
ADQAPI.ADQAPI_GetRevision()
|
||||
|
||||
# Manually set return type from some ADQAPI functions
|
||||
ADQAPI.CreateADQControlUnit.restype = ct.c_void_p
|
||||
ADQAPI.ADQ_GetRevision.restype = ct.c_void_p
|
||||
ADQAPI.ADQ_GetPtrStream.restype = ct.POINTER(ct.c_int16)
|
||||
ADQAPI.ADQControlUnit_FindDevices.argtypes = [ct.c_void_p]
|
||||
# Create ADQControlUnit
|
||||
self.adq_cu = ct.c_void_p(ADQAPI.CreateADQControlUnit())
|
||||
ADQAPI.ADQControlUnit_EnableErrorTrace(self.adq_cu, 3, '.')
|
||||
|
||||
# Find ADQ devices
|
||||
ADQAPI.ADQControlUnit_FindDevices(self.adq_cu)
|
||||
n_of_adq = ADQAPI.ADQControlUnit_NofADQ(self.adq_cu)
|
||||
if n_of_adq != 1:
|
||||
print('number of ADQs must be 1, not %d' % n_of_adq)
|
||||
print('it seems the ADQ was not properly closed')
|
||||
print('please try again or reboot')
|
||||
sys.exit(0)
|
||||
atexit.register(self.deletecu)
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
|
||||
rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num)
|
||||
revision = ct.cast(rev, ct.POINTER(ct.c_int))
|
||||
out = [f'Connected to ADQ #1, FPGA Revision: {revision[0]}']
|
||||
if revision[1]:
|
||||
out.append('Local copy')
|
||||
else:
|
||||
if revision[2]:
|
||||
out.append('SVN Managed - Mixed Revision')
|
||||
else:
|
||||
out.append('SVN Updated')
|
||||
print(', '.join(out))
|
||||
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF)
|
||||
|
||||
##########################
|
||||
# Test pattern
|
||||
# ADQAPI.ADQ_SetTestPatternMode(self.adq_cu, self.adq_num, 4)
|
||||
##########################
|
||||
# Sample skip
|
||||
# ADQAPI.ADQ_SetSampleSkip(self.adq_cu, self.adq_num, 1)
|
||||
##########################
|
||||
|
||||
# set trigger mode
|
||||
if not ADQAPI.ADQ_SetTriggerMode(self.adq_cu, self.adq_num, self.trigger):
|
||||
raise RuntimeError('ADQ_SetTriggerMode failed.')
|
||||
if self.trigger == LVL_TRIG:
|
||||
if not ADQAPI.ADQ_SetLvlTrigLevel(self.adq_cu, self.adq_num, -100):
|
||||
raise RuntimeError('ADQ_SetLvlTrigLevel failed.')
|
||||
if not ADQAPI.ADQ_SetTrigLevelResetValue(self.adq_cu, self.adq_num, 1000):
|
||||
raise RuntimeError('ADQ_SetTrigLevelResetValue failed.')
|
||||
if not ADQAPI.ADQ_SetLvlTrigChannel(self.adq_cu, self.adq_num, 1):
|
||||
raise RuntimeError('ADQ_SetLvlTrigChannel failed.')
|
||||
if not ADQAPI.ADQ_SetLvlTrigEdge(self.adq_cu, self.adq_num, LVL_RISING):
|
||||
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
|
||||
elif self.trigger == EXT_TRIG_1:
|
||||
if not ADQAPI.ADQ_SetExternTrigEdge(self.adq_cu, self.adq_num, 2):
|
||||
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
|
||||
# if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)):
|
||||
# raise RuntimeError('SetTriggerThresholdVoltage failed.')
|
||||
# proabably the folloiwng is wrong.
|
||||
# print("CHANNEL:" + str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
|
||||
|
||||
def init(self, samples_per_record=None, number_of_records=None):
|
||||
"""initialize dimensions and store result object"""
|
||||
if samples_per_record:
|
||||
self.samples_per_record = samples_per_record
|
||||
if number_of_records:
|
||||
self.number_of_records = number_of_records
|
||||
# Setup target buffers for data
|
||||
self.target_buffers = (ct.POINTER(ct.c_int16 * self.samples_per_record * self.number_of_records)
|
||||
* self.max_number_of_channels)()
|
||||
for bufp in self.target_buffers:
|
||||
bufp.contents = (ct.c_int16 * self.samples_per_record * self.number_of_records)()
|
||||
|
||||
def deletecu(self):
|
||||
cu = self.__dict__.pop('adq_cu', None)
|
||||
if cu is None:
|
||||
return
|
||||
print('shut down ADQ')
|
||||
# Only disarm trigger after data is collected
|
||||
ADQAPI.ADQ_DisarmTrigger(cu, self.adq_num)
|
||||
ADQAPI.ADQ_MultiRecordClose(cu, self.adq_num)
|
||||
# Delete ADQControlunit
|
||||
ADQAPI.DeleteADQControlUnit(cu)
|
||||
print('ADQ closed')
|
||||
|
||||
def start(self, data):
|
||||
# Start acquisition
|
||||
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
|
||||
self.number_of_records,
|
||||
self.samples_per_record)
|
||||
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
|
||||
self.data = data
|
||||
|
||||
def get_data(self):
|
||||
"""get new data if available"""
|
||||
ready = False
|
||||
data = self.data
|
||||
if not data:
|
||||
self.busy = False
|
||||
return None # no new data
|
||||
|
||||
if ADQAPI.ADQ_GetAcquiredAll(self.adq_cu, self.adq_num):
|
||||
ready = True
|
||||
data.timer('ready')
|
||||
else:
|
||||
if self.trigger == SW_TRIG:
|
||||
ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num)
|
||||
if not ready:
|
||||
self.busy = True
|
||||
return None
|
||||
self.data = None
|
||||
t = time.time()
|
||||
# Get data from ADQ
|
||||
if not ADQAPI.ADQ_GetData(
|
||||
self.adq_cu, self.adq_num, self.target_buffers,
|
||||
self.samples_per_record * self.number_of_records, 2,
|
||||
0, self.number_of_records, ADQ_CHANNELS_MASK,
|
||||
0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL):
|
||||
raise RuntimeError('no success from ADQ_GetDATA')
|
||||
data.retrieve(self)
|
||||
return data
|
||||
|
||||
|
||||
class PEdata:
|
||||
def __init__(self, adq):
|
||||
self.sample_rate = adq.sample_rate
|
||||
self.samp_freq = self.sample_rate / GHz
|
||||
self.number_of_records = adq.number_of_records
|
||||
self.timer = Timer()
|
||||
|
||||
def retrieve(self, adq):
|
||||
data = []
|
||||
rawsignal = []
|
||||
for ch in range(2):
|
||||
onedim = np.frombuffer(adq.target_buffers[ch].contents, dtype=np.int16)
|
||||
rawsignal.append(onedim[:adq.samples_per_record])
|
||||
# convert 16 bit int to a value in the range -1 .. 1
|
||||
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2 ** 15))
|
||||
# Now this is an array with all records, but the time is artificial
|
||||
self.data = data
|
||||
self.rawsignal = rawsignal
|
||||
self.timer('retrieved')
|
||||
|
||||
def sinW(self, sig, freq, ti, tf):
|
||||
# sig: signal array
|
||||
# freq
|
||||
# ti, tf: initial and end time
|
||||
si = int(ti * self.samp_freq)
|
||||
nperiods = freq * (tf - ti)
|
||||
n = int(round(max(2, int(nperiods)) / nperiods * (tf-ti) * self.samp_freq))
|
||||
self.nperiods = n
|
||||
t = np.arange(si, len(sig)) / self.samp_freq
|
||||
t = t[:n]
|
||||
self.pulselen = n / self.samp_freq
|
||||
sig = sig[si:si+n]
|
||||
a = 2*np.sum(sig*np.cos(2*np.pi*freq*t))/len(sig)
|
||||
b = 2*np.sum(sig*np.sin(2*np.pi*freq*t))/len(sig)
|
||||
return a, b
|
||||
|
||||
def mix(self, sigin, sigout, freq, ti, tf):
|
||||
# sigin, sigout: signal array, incomping, output
|
||||
# freq
|
||||
# ti, tf: initial and end time of sigin
|
||||
a, b = self.sinW(sigin, freq, ti, tf)
|
||||
amp = np.sqrt(a**2 + b**2)
|
||||
a, b = a/amp, b/amp
|
||||
# si = int(ti * self.samp_freq)
|
||||
t = np.arange(len(sigout)) / self.samp_freq
|
||||
wave1 = sigout * (a * np.cos(2*np.pi*freq*t) + b * np.sin(2*np.pi*freq*t))
|
||||
wave2 = sigout * (a * np.sin(2*np.pi*freq*t) - b * np.cos(2*np.pi*freq*t))
|
||||
return wave1, wave2
|
||||
|
||||
def averageiq(self, data, freq, ti, tf):
|
||||
"""Average over records"""
|
||||
iorq = np.array([self.mix(data[0][i], data[1][i], freq, ti, tf) for i in range(self.number_of_records)])
|
||||
return iorq.sum(axis=0) / self.number_of_records
|
||||
|
||||
def filtro(self, iorq, cutoff):
|
||||
# butter lowpass
|
||||
nyq = 0.5 * self.sample_rate
|
||||
normal_cutoff = cutoff / nyq
|
||||
order = 5
|
||||
b, a = butter(order, normal_cutoff, btype='low', analog=False)
|
||||
iqf = [filtfilt(b, a, iorq[i]) for i in np.arange(len(iorq))]
|
||||
return iqf
|
||||
|
||||
def box(self, iorq, ti, tf):
|
||||
si = int(self.samp_freq * ti)
|
||||
sf = int(self.samp_freq * tf)
|
||||
bxa = [sum(iorq[i][si:sf])/(sf-si) for i in np.arange(len(iorq))]
|
||||
return bxa
|
||||
|
||||
def gates_and_curves(self, freq, pulse, roi, bw_cutoff):
|
||||
"""return iq values of rois and prepare plottable curves for iq"""
|
||||
self.timer('gates')
|
||||
try:
|
||||
self.ndecimate = int(round(self.sample_rate / freq))
|
||||
except TypeError as e:
|
||||
raise TypeError(f'{self.sample_rate}/{freq} {e}')
|
||||
iq = self.averageiq(self.data, freq / GHz, *pulse)
|
||||
self.timer('aviq')
|
||||
iqf = self.filtro(iq, bw_cutoff)
|
||||
self.timer('filtro')
|
||||
m = max(1, len(iqf[0]) // self.ndecimate)
|
||||
ll = m * self.ndecimate
|
||||
iqf = [iqfx[0:ll] for iqfx in iqf]
|
||||
self.timer('iqf')
|
||||
iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2)
|
||||
self.timer('avg')
|
||||
t_axis = np.arange(m) * self.ndecimate / self.samp_freq
|
||||
pulsig = np.abs(self.data[0][0])
|
||||
self.timer('pulsig')
|
||||
pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1)
|
||||
result = ([self.box(iqf, *r) for r in roi], # gates
|
||||
(t_axis, iqd[0], iqd[1], pulsig)) # curves
|
||||
self.timer('result')
|
||||
# self.timer.show()
|
||||
# ns = len(self.rawsignal[0]) * self.number_of_records
|
||||
# print(f'{ns} {ns / 2e6} ms')
|
||||
return result
|
||||
|
||||
|
||||
class Namespace:
|
||||
"""holds channel or other data"""
|
||||
def __init__(self, **kwds):
|
||||
self.__dict__.update(**kwds)
|
||||
|
||||
|
||||
class RUSdata:
|
||||
def __init__(self, adq, freq, periods, delay_samples):
|
||||
self.sample_rate = adq.sample_rate
|
||||
self.freq = freq
|
||||
self.periods = periods
|
||||
self.delay_samples = delay_samples
|
||||
self.samples_per_record = adq.samples_per_record
|
||||
self.inp = Namespace(idx=0, name='input')
|
||||
self.out = Namespace(idx=1, name='output')
|
||||
self.channels = (self.inp, self.out)
|
||||
self.timer = Timer()
|
||||
|
||||
def retrieve(self, adq):
|
||||
self.timer('start retrieve')
|
||||
npts = self.samples_per_record - self.delay_samples
|
||||
nbin = max(1, npts // (self.periods * 60)) # for performance reasons, do the binning first
|
||||
nreduced = npts // nbin
|
||||
ft = 2 * np.pi * self.freq * nbin / self.sample_rate * np.arange(nreduced)
|
||||
self.timer('create time axis')
|
||||
# complex_sinusoid = np.exp(1j * ft) # do not use this, below is 33 % faster
|
||||
complex_sinusoid = 1j * np.sin(ft) + np.cos(ft)
|
||||
self.timer('sinusoid')
|
||||
|
||||
rawsignal = [] # for raw plot
|
||||
for chan in self.channels: # looping over input and output
|
||||
# although the ADC is only 14 bit it is represented as unsigend 16 bit numbers,
|
||||
# and due to some calculations (calibration) the last 2 bits are not zero
|
||||
beg = self.delay_samples
|
||||
isignal = np.frombuffer(adq.target_buffers[chan.idx].contents, dtype=np.int16)[beg:beg+nreduced * nbin]
|
||||
self.timer('isignal')
|
||||
reduced = isignal.reshape((-1, nbin)).mean(axis=1) # this converts also int16 to float
|
||||
self.timer('reduce')
|
||||
rawsignal.append(reduced)
|
||||
chan.signal = signal = reduced * 2 ** -16 # in V -> peak to peak 1 V ~ +- 0.5 V
|
||||
self.timer('divide')
|
||||
# calculate RMS * sqrt(2) -> peak sinus amplitude.
|
||||
# may be higher than the input range by a factor 1.4 when heavily clipped
|
||||
chan.amplitude = np.sqrt((signal ** 2).mean()) * RMS_TO_VPP
|
||||
self.timer('amp')
|
||||
chan.mixed = signal * complex_sinusoid
|
||||
self.timer('mix')
|
||||
chan.mean = chan.mixed.mean()
|
||||
self.timer('mean')
|
||||
self.rawsignal = rawsignal
|
||||
if self.inp.mean:
|
||||
self.iq = self.out.mean / self.inp.mean
|
||||
else:
|
||||
self.iq = 0
|
||||
|
||||
def get_quality(self):
|
||||
"""get signal quality info
|
||||
|
||||
quality info (small values indicate good quality):
|
||||
- input_stddev:
|
||||
the imaginary part indicates deviations in phase
|
||||
the real part indicates deviations in amplitude
|
||||
- output_slope:
|
||||
the imaginary part indicates a turning phase (rad/sec)
|
||||
the real part indicates changes in amplitude (0.01 ~= 1%/sec)
|
||||
"""
|
||||
self.timer('get_quality')
|
||||
npts = len(self.channels[0].signal)
|
||||
nper = npts // self.periods
|
||||
for chan in self.channels:
|
||||
mean = chan.mixed.mean()
|
||||
chan.reduced = chan.mixed[:self.periods * nper].reshape((-1, nper)).mean(axis=1) / mean
|
||||
|
||||
timeaxis = np.arange(len(self.out.reduced)) * self.sample_rate / self.freq
|
||||
result = Namespace(
|
||||
input_stddev=self.inp.reduced.std(),
|
||||
output_slope=np.polyfit(timeaxis, self.out.reduced, 1)[0])
|
||||
self.timer('got_quality')
|
||||
self.timer.show()
|
||||
ns = len(self.rawsignal[0])
|
||||
print(f'{ns} {ns / 2e6} ms')
|
||||
return result
|
||||
|
131
frappy_psi/autofill.py
Normal file
131
frappy_psi/autofill.py
Normal file
@ -0,0 +1,131 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import time
|
||||
from frappy.core import Attached, Readable, Writable, Parameter, Command, \
|
||||
IDLE, BUSY, DISABLED, ERROR
|
||||
from frappy.datatypes import FloatRange, StatusType, TupleOf, EnumType
|
||||
from frappy.states import HasStates, Retry, status_code
|
||||
|
||||
|
||||
class AutoFill(HasStates, Readable):
|
||||
level = Attached(Readable)
|
||||
valve = Attached(Writable)
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
mode = Parameter('auto mode', EnumType(disabled=0, auto=30), readonly=False)
|
||||
|
||||
fill_level = Parameter('low threshold triggering start filling',
|
||||
FloatRange(unit='%'), readonly=False)
|
||||
full_level = Parameter('high threshold triggering stop filling',
|
||||
FloatRange(unit='%'), readonly=False)
|
||||
fill_minutes_range = Parameter('range of possible fill rate',
|
||||
TupleOf(FloatRange(unit='min'), FloatRange(unit='min')),
|
||||
readonly=False)
|
||||
hold_hours_range = Parameter('range of possible consumption rate',
|
||||
TupleOf(FloatRange(unit='h'), FloatRange(unit='h')),
|
||||
readonly=False)
|
||||
fill_delay = Parameter('delay for cooling the transfer line',
|
||||
FloatRange(unit='min'), readonly=False)
|
||||
|
||||
def read_status(self):
|
||||
if self.mode == 'DISABLED':
|
||||
return DISABLED, ''
|
||||
vstatus = self.valve.status
|
||||
if vstatus[0] // 100 != IDLE // 100:
|
||||
self.stop_machine(vstatus)
|
||||
return vstatus
|
||||
status = self.level.read_status(self)
|
||||
if status[0] // 100 == IDLE // 100:
|
||||
return HasStates.read_status(self)
|
||||
self.stop_machine(status)
|
||||
return status
|
||||
|
||||
def write_mode(self, mode):
|
||||
if mode == 'DISABLED':
|
||||
self.stop_machine((DISABLED, ''))
|
||||
elif mode == 'AUTO':
|
||||
self.start_machine(self.watching)
|
||||
return mode
|
||||
|
||||
@status_code(BUSY)
|
||||
def watching(self, state):
|
||||
if state.init:
|
||||
self.valve.write_target(0)
|
||||
delta = state.delta(10)
|
||||
raw = self.level.value
|
||||
if raw > self.value:
|
||||
self.value -= delta / (3600 * self.hold_hours_range[1])
|
||||
elif raw < self.value:
|
||||
self.value -= delta / (3600 * self.hold_hours_range[0])
|
||||
else:
|
||||
self.value = raw
|
||||
if self.value < self.fill_level:
|
||||
return self.precooling
|
||||
return Retry
|
||||
|
||||
@status_code(BUSY)
|
||||
def precooling(self, state):
|
||||
if state.init:
|
||||
state.fillstart = state.now
|
||||
self.valve.write_target(1)
|
||||
delta = state.delta(1)
|
||||
raw = self.level.value
|
||||
if raw > self.value:
|
||||
self.value += delta / (60 * self.fill_minutes_range[0])
|
||||
elif raw < self.value:
|
||||
self.value -= delta / (60 * self.fill_minutes_range[0])
|
||||
else:
|
||||
self.value = raw
|
||||
if self.value > self.full_level:
|
||||
return self.watching
|
||||
if state.now > state.fillstart + self.fill_delay * 60:
|
||||
return self.filling
|
||||
return Retry
|
||||
|
||||
@status_code(BUSY)
|
||||
def filling(self, state):
|
||||
delta = state.delta(1)
|
||||
raw = self.level.value
|
||||
if raw > self.value:
|
||||
self.value += delta / (60 * self.fill_minutes_range[0])
|
||||
elif raw < self.value:
|
||||
self.value += delta / (60 * self.fill_minutes_range[1])
|
||||
else:
|
||||
self.value = raw
|
||||
if self.value > self.full_level:
|
||||
return self.watching
|
||||
return Retry
|
||||
|
||||
def on_cleanup(self, state):
|
||||
try:
|
||||
self.valve.write_target(0)
|
||||
except Exception:
|
||||
pass
|
||||
super().on_cleanup()
|
||||
|
||||
@Command()
|
||||
def fill(self):
|
||||
self.mode = 'AUTO'
|
||||
self.start_machine(self.precooling, fillstart=time.time())
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
self.start_machine(self.watching)
|
@ -66,8 +66,9 @@ class Power(HasIO, Readable):
|
||||
|
||||
|
||||
class Output(HasIO, Writable):
|
||||
value = Parameter(datatype=FloatRange(0,100,unit='%'))
|
||||
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)
|
||||
maxvolt = Parameter('voltage at 100%',datatype=FloatRange(0,60,unit='V'),default=50,readonly=False)
|
||||
maxcurrent = Parameter('current at 100%',datatype=FloatRange(0,5,unit='A'),default=2,readonly=False)
|
||||
output_enable = Parameter('control on/off', BoolType(), readonly=False)
|
||||
@ -78,8 +79,10 @@ class Output(HasIO, Writable):
|
||||
|
||||
def write_target(self, target):
|
||||
self.write_output_enable(target != 0)
|
||||
self.communicate(f'VOLT{round(max(8,target*self.maxvolt/10)):03d}')
|
||||
self.communicate(f'CURR{round(target*self.maxcurrent):03d}')
|
||||
self.communicate(f'VOLT{round(max(8,(target)**0.5 * self.maxvolt)):03d}')
|
||||
self.communicate(f'CURR{round((target)**0.5* 10 * self.maxcurrent):03d}')
|
||||
#self.communicate(f'VOLT{round(max(8,target*self.maxvolt/10)):03d}')
|
||||
#self.communicate(f'CURR{round(target*self.maxcurrent):03d}')
|
||||
self.value = target
|
||||
|
||||
def write_output_enable(self, value):
|
||||
|
128
frappy_psi/butterflyvalve.py
Normal file
128
frappy_psi/butterflyvalve.py
Normal file
@ -0,0 +1,128 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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:
|
||||
# M. Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.core import Attached, Command, EnumType, FloatRange, \
|
||||
Drivable, Parameter, BUSY, IDLE, ERROR
|
||||
|
||||
|
||||
class Valve(Drivable):
|
||||
motor = Attached(Drivable) # refers to motor module
|
||||
value = Parameter('valve state',
|
||||
EnumType(closed=0, open=1, undefined=2),
|
||||
default=2)
|
||||
status = Parameter() # inherit properties from Drivable
|
||||
target = Parameter('valve target',
|
||||
EnumType(closed=0, open=1),
|
||||
readonly=False)
|
||||
# TODO: convert to properties after tests
|
||||
open_pos = Parameter('target position for open state', FloatRange(), readonly=False, default=80)
|
||||
mid_pos = Parameter('position for changing speed', FloatRange(), readonly=False, default=5)
|
||||
fast_speed = Parameter('normal speed', FloatRange(), readonly=False, default=40)
|
||||
slow_speed = Parameter('reduced speed', FloatRange(), readonly=False, default=10)
|
||||
__motor_target = None
|
||||
__status = IDLE, ''
|
||||
__value = 'undefined'
|
||||
__drivestate = 0 # 2 when driving to intermediate target or on retry, 1 when driving to final target, 0 when idle
|
||||
|
||||
def doPoll(self):
|
||||
mot = self.motor
|
||||
motpos = mot.read_value()
|
||||
scode, stext = mot.read_status()
|
||||
drivestate = self.__drivestate
|
||||
if scode >= ERROR:
|
||||
if self.__drivestate and self.__remaining_tries > 0:
|
||||
drivestate = 2
|
||||
self.__remaining_tries -= 1
|
||||
mot.reset()
|
||||
mot.write_speed(self.slow_speed)
|
||||
self.__status = BUSY, f'retry {self._action}'
|
||||
else:
|
||||
self.__status = ERROR, f'valve motor: {stext}'
|
||||
elif scode < BUSY:
|
||||
if self.__motor_target is not None and mot.target != self.__motor_target:
|
||||
self.__status = ERROR, 'motor was driven directly'
|
||||
elif drivestate == 2:
|
||||
self.goto(self.target)
|
||||
drivestate = 1
|
||||
else:
|
||||
if -3 < motpos < 3:
|
||||
self.__value = 'closed'
|
||||
self.__status = IDLE, ''
|
||||
elif self.open_pos * 0.5 < motpos < self.open_pos * 1.5:
|
||||
self.__value = 'open'
|
||||
self.__status = IDLE, ''
|
||||
else:
|
||||
self.__status = ERROR, 'undefined'
|
||||
if self.__drivestate and not self.isBusy(self.__status):
|
||||
drivestate = 0
|
||||
self.__motor_target = None
|
||||
self.setFastPoll(False)
|
||||
self.__drivestate = drivestate
|
||||
self.read_status()
|
||||
self.read_value()
|
||||
|
||||
def read_status(self):
|
||||
return self.__status
|
||||
|
||||
def read_value(self):
|
||||
if self.read_status()[0] >= BUSY:
|
||||
return 'undefined'
|
||||
return self.__value
|
||||
|
||||
def goto(self, target):
|
||||
"""go to open, closed or intermediate position
|
||||
|
||||
the intermediate position is targeted when a speed change is needed
|
||||
|
||||
return 2 when a retry is needed, 1 else
|
||||
"""
|
||||
mot = self.motor
|
||||
if target: # 'open'
|
||||
self._action = 'opening'
|
||||
if True or mot.value > self.mid_pos:
|
||||
mot.write_speed(self.fast_speed)
|
||||
self.__motor_target = mot.write_target(self.open_pos)
|
||||
return 1
|
||||
mot.write_speed(self.slow_speed)
|
||||
self.__motor_target = mot.write_target(self.mid_pos)
|
||||
return 2
|
||||
self._action = 'closing'
|
||||
if mot.value > self.mid_pos * 2:
|
||||
mot.write_speed(self.fast_speed)
|
||||
self.__motor_target = mot.write_target(self.mid_pos)
|
||||
return 2
|
||||
mot.write_speed(self.slow_speed)
|
||||
self.__motor_target = mot.write_target(0)
|
||||
return 1
|
||||
|
||||
def write_target(self, target):
|
||||
self.__remaining_tries = 5
|
||||
self.__drivestate = self.goto(target)
|
||||
self.__status = BUSY, self._action
|
||||
self.read_status()
|
||||
self.read_value()
|
||||
self.setFastPoll(True)
|
||||
|
||||
@Command() # python decorator to mark it as a command
|
||||
def stop(self):
|
||||
"""stop the motor -> value might get undefined"""
|
||||
self.__drivestate = 0
|
||||
self.motor.stop()
|
@ -22,6 +22,7 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from os.path import basename, dirname, exists, join
|
||||
|
||||
import numpy as np
|
||||
@ -31,13 +32,22 @@ from scipy.interpolate import PchipInterpolator, CubicSpline, PPoly # pylint: d
|
||||
from frappy.errors import ProgrammingError, RangeError
|
||||
from frappy.lib import clamp
|
||||
|
||||
|
||||
def identity(x):
|
||||
return x
|
||||
|
||||
|
||||
def exp10(x):
|
||||
return 10 ** np.array(x)
|
||||
|
||||
|
||||
to_scale = {
|
||||
'lin': lambda x: x,
|
||||
'log': lambda x: np.log10(x),
|
||||
'lin': identity,
|
||||
'log': np.log10,
|
||||
}
|
||||
from_scale = {
|
||||
'lin': lambda x: x,
|
||||
'log': lambda x: 10 ** np.array(x),
|
||||
'lin': identity,
|
||||
'log': exp10,
|
||||
}
|
||||
TYPES = [ # lakeshore type, inp-type, loglog
|
||||
('DT', 'si', False), # Si diode
|
||||
@ -55,7 +65,7 @@ TYPES = [ # lakeshore type, inp-type, loglog
|
||||
|
||||
OPTION_TYPE = {
|
||||
'loglog': 0, # boolean
|
||||
'extrange': 2, # tuple(min T, max T for extrapolation
|
||||
'extrange': 2, # tuple(min T, max T) for extrapolation
|
||||
'calibrange': 2, # tuple(min T, max T)
|
||||
}
|
||||
|
||||
@ -222,14 +232,6 @@ PARSERS = {
|
||||
}
|
||||
|
||||
|
||||
def check(x, y, islog):
|
||||
# check interpolation error
|
||||
yi = y[:-2] + (x[1:-1] - x[:-2]) * (y[2:] - y[:-2]) / (x[2:] - x[:-2])
|
||||
if islog:
|
||||
return sum((yi - y[1:-1]) ** 2)
|
||||
return sum((np.log10(yi) - np.log10(y[1:-1])) ** 2)
|
||||
|
||||
|
||||
def get_curve(newscale, curves):
|
||||
"""get curve from curve cache (converts not existing ones)
|
||||
|
||||
@ -247,6 +249,7 @@ def get_curve(newscale, curves):
|
||||
class CalCurve(HasOptions):
|
||||
EXTRAPOLATION_AMOUNT = 0.1
|
||||
MAX_EXTRAPOLATION_FACTOR = 2
|
||||
filename = None # calibration file
|
||||
|
||||
def __init__(self, calibspec=None, *, x=None, y=None, cubic_spline=True, **options):
|
||||
"""calibration curve
|
||||
@ -257,7 +260,7 @@ class CalCurve(HasOptions):
|
||||
[<full path> | <name>][,<key>=<value> ...]
|
||||
for <key>/<value> as in parser arguments
|
||||
:param x, y: x and y arrays (given instead of calibspec)
|
||||
:param cubic_split: set to False for always using Pchip interpolation
|
||||
:param cubic_spline: set to False for always using Pchip interpolation
|
||||
:param options: options for parsers
|
||||
"""
|
||||
self.options = options
|
||||
@ -265,26 +268,31 @@ class CalCurve(HasOptions):
|
||||
parser = StdParser()
|
||||
parser.xdata = x
|
||||
parser.ydata = y
|
||||
self.calibname = 'custom'
|
||||
else:
|
||||
if x or y:
|
||||
raise ProgrammingError('can not give both calibspec and x,y ')
|
||||
sensopt = calibspec.split(',')
|
||||
calibname = sensopt.pop(0)
|
||||
_, dot, ext = basename(calibname).rpartition('.')
|
||||
self.calibname = basename(calibname)
|
||||
head, dot, ext = self.calibname.rpartition('.')
|
||||
if dot:
|
||||
self.calibname = head
|
||||
kind = None
|
||||
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(':')
|
||||
pathlist.append(join(dirname(__file__), 'calcurves'))
|
||||
pathlist = [Path(p.strip()) for p in os.environ.get('FRAPPY_CALIB_PATH', '').split(':')]
|
||||
pathlist.append(Path(dirname(__file__)) / 'calcurves')
|
||||
for path in pathlist:
|
||||
# first try without adding kind
|
||||
filename = join(path.strip(), calibname)
|
||||
if exists(filename):
|
||||
filename = path / calibname
|
||||
if filename.exists():
|
||||
kind = ext if dot else None
|
||||
break
|
||||
# then try adding all kinds as extension
|
||||
for nam in calibname, calibname.upper(), calibname.lower():
|
||||
for kind in PARSERS:
|
||||
filename = join(path.strip(), '%s.%s' % (nam, kind))
|
||||
filename = path / f'{nam}.{kind}'
|
||||
if exists(filename):
|
||||
self.filename = filename
|
||||
break
|
||||
else:
|
||||
continue
|
||||
@ -328,6 +336,7 @@ class CalCurve(HasOptions):
|
||||
not_incr_idx = np.argwhere(x[1:] <= x[:-1])
|
||||
if len(not_incr_idx):
|
||||
raise RangeError('x not monotonic at x=%.4g' % x[not_incr_idx[0]])
|
||||
self.ptc = y[-1] > y[0]
|
||||
|
||||
self.x = {parser.xscale: x}
|
||||
self.y = {parser.yscale: y}
|
||||
@ -344,8 +353,7 @@ class CalCurve(HasOptions):
|
||||
self.convert_x = to_scale[newscale]
|
||||
self.convert_y = from_scale[newscale]
|
||||
self.calibrange = self.options.get('calibrange')
|
||||
dirty = set()
|
||||
self.extra_points = False
|
||||
self.extra_points = (0, 0)
|
||||
self.cutted = False
|
||||
if self.calibrange:
|
||||
self.calibrange = sorted(self.calibrange)
|
||||
@ -371,7 +379,6 @@ class CalCurve(HasOptions):
|
||||
self.y = {newscale: y}
|
||||
ibeg = 0
|
||||
iend = len(x)
|
||||
dirty.add('xy')
|
||||
else:
|
||||
self.extra_points = ibeg, len(x) - iend
|
||||
else:
|
||||
@ -493,13 +500,48 @@ class CalCurve(HasOptions):
|
||||
except IndexError:
|
||||
return defaultx
|
||||
|
||||
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None):
|
||||
def interpolation_error(self, x0, x1, y0, y1, funx, funy, relerror, return_tuple=False):
|
||||
"""calcualte interpoaltion error
|
||||
|
||||
:param x0: start of interval
|
||||
:param x1: end of interval
|
||||
:param y0: y at start of interval
|
||||
:param y1: y at end of interval
|
||||
:param funx: function to convert x from exported scale to internal scale
|
||||
:param funy: function to convert y from internal scale to exported scale
|
||||
:param relerror: True when the exported y scale is linear
|
||||
:param return_tuple: True: return interpolation error as a tuple with two values
|
||||
(without and with 3 additional points)
|
||||
False: return one value without additional points
|
||||
:return: relative deviation
|
||||
"""
|
||||
xspace = np.linspace(x0, x1, 9)
|
||||
x = funx(xspace)
|
||||
yr = self.spline(x)
|
||||
yspline = funy(yr)
|
||||
yinterp = y0 + np.linspace(0.0, y1 - y0, 9)
|
||||
# difference between spline (at m points) and liner interpolation
|
||||
diff = np.abs(yspline - yinterp)
|
||||
# estimate of interpolation error with 4 sections:
|
||||
# difference between spline (at m points) and linear interpolation between neighboring points
|
||||
|
||||
if relerror:
|
||||
fact = 2 / (np.abs(y0) + np.abs(y1)) # division by zero can not happen, as y0 and y1 can not both be zero
|
||||
else:
|
||||
fact = 2.3 # difference is in log10 -> multiply by 1 / log10(e)
|
||||
result = np.max(diff, axis=0) * fact
|
||||
if return_tuple:
|
||||
diff2 = np.abs(0.5 * (yspline[:-2:2] + yspline[2::2]) - funy(yr[1:-1:2]))
|
||||
return result, np.max(diff2, axis=0) * fact
|
||||
return result
|
||||
|
||||
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None, nmin=199):
|
||||
"""export curve for downloading to hardware
|
||||
|
||||
:param nmax: max number of points. if the number of given points is bigger,
|
||||
the points with the lowest interpolation error are omitted
|
||||
:param logformat: a list with two elements of None, True or False
|
||||
True: use log, False: use line, None: use log if self.loglog
|
||||
:param logformat: a list with two elements of None, True or False for x and y
|
||||
True: use log, False: use lin, None: use log if self.loglog
|
||||
values None are replaced with the effectively used format
|
||||
False / True are replaced by [False, False] / [True, True]
|
||||
default is False
|
||||
@ -507,25 +549,26 @@ class CalCurve(HasOptions):
|
||||
:param extrapolate: a flag indicating whether the curves should be extrapolated
|
||||
to the preset extrapolation range
|
||||
:param xlimits: max x range
|
||||
:param nmin: minimum number of points
|
||||
:return: numpy array with 2 dimensions returning the curve
|
||||
"""
|
||||
|
||||
if logformat in (True, False):
|
||||
logformat = [logformat, logformat]
|
||||
logformat = (logformat, logformat)
|
||||
self.logformat = list(logformat)
|
||||
try:
|
||||
scales = []
|
||||
for idx, logfmt in enumerate(logformat):
|
||||
if logfmt and self.lin_forced[idx]:
|
||||
raise ValueError('%s must contain positive values only' % 'xy'[idx])
|
||||
logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
|
||||
self.logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
|
||||
scales.append('log' if linlog else 'lin')
|
||||
xscale, yscale = scales
|
||||
except (TypeError, AssertionError):
|
||||
raise ValueError('logformat must be a 2 element list or a boolean')
|
||||
raise ValueError('logformat must be a 2 element sequence or a boolean')
|
||||
|
||||
x = self.spline.x[1:-1] # raw units, excluding extrapolated points
|
||||
x1, x2 = xmin, xmax = x[0], x[-1]
|
||||
y1, y2 = sorted(self.spline([x1, x2]))
|
||||
xr = self.spline.x[1:-1] # raw units, excluding extrapolated points
|
||||
x1, x2 = xmin, xmax = xr[0], xr[-1]
|
||||
|
||||
if extrapolate and not yrange:
|
||||
yrange = self.exty
|
||||
@ -535,42 +578,100 @@ class CalCurve(HasOptions):
|
||||
lim = to_scale[self.scale](xlimits)
|
||||
xmin = clamp(xmin, *lim)
|
||||
xmax = clamp(xmax, *lim)
|
||||
# start and end index of calibrated range
|
||||
ibeg, iend = self.extra_points[0], len(xr) - self.extra_points[1]
|
||||
if xmin != x1 or xmax != x2:
|
||||
ibeg, iend = np.searchsorted(x, (xmin, xmax))
|
||||
if abs(x[ibeg] - xmin) < 0.1 * (x[ibeg + 1] - x[ibeg]):
|
||||
i, j = np.searchsorted(xr, (xmin, xmax))
|
||||
if abs(xr[i] - xmin) < 0.1 * (xr[i + 1] - xr[i]):
|
||||
# remove first point, if close
|
||||
ibeg += 1
|
||||
if abs(x[iend - 1] - xmax) < 0.1 * (x[iend - 1] - x[iend - 2]):
|
||||
i += 1
|
||||
if abs(xr[j - 1] - xmax) < 0.1 * (xr[j - 1] - xr[j - 2]):
|
||||
# remove last point, if close
|
||||
iend -= 1
|
||||
x = np.concatenate(([xmin], x[ibeg:iend], [xmax]))
|
||||
y = self.spline(x)
|
||||
j -= 1
|
||||
offset = i - 1
|
||||
xr = np.concatenate(([xmin], xr[i:j], [xmax]))
|
||||
ibeg = max(0, ibeg - offset)
|
||||
iend = min(len(xr), iend - offset)
|
||||
|
||||
yr = self.spline(xr)
|
||||
|
||||
# convert to exported scale
|
||||
if xscale != self.scale:
|
||||
x = to_scale[xscale](from_scale[self.scale](x))
|
||||
if yscale != self.scale:
|
||||
y = to_scale[yscale](from_scale[self.scale](y))
|
||||
|
||||
# reduce number of points, if needed
|
||||
n = len(x)
|
||||
i, j = 1, n - 1 # index range for calculating interpolation deviation
|
||||
deviation = np.zeros(n)
|
||||
while True:
|
||||
# calculate interpolation error when a single point is omitted
|
||||
ym = y[i-1:j-1] + (x[i:j] - x[i-1:j-1]) * (y[i+1:j+1] - y[i-1:j-1]) / (x[i+1:j+1] - x[i-1:j-1])
|
||||
if yscale == 'log':
|
||||
deviation[i:j] = np.abs(ym - y[i:j])
|
||||
if xscale == self.scale:
|
||||
xbwd = identity
|
||||
x = xr
|
||||
else:
|
||||
if self.scale == 'log':
|
||||
xfwd, xbwd = from_scale[self.scale], to_scale[self.scale]
|
||||
else:
|
||||
deviation[i:j] = np.abs(ym - y[i:j]) / (np.abs(ym + y[i:j]) + 1e-10)
|
||||
if n <= nmax:
|
||||
break
|
||||
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
|
||||
y = np.delete(y, idx)
|
||||
x = np.delete(x, idx)
|
||||
deviation = np.delete(deviation, idx)
|
||||
n -= 1
|
||||
# index range to recalculate
|
||||
i, j = max(1, idx - 1), min(n - 1, idx + 1)
|
||||
self.deviation = deviation # for debugging purposes
|
||||
xfwd, xbwd = to_scale[xscale], from_scale[xscale]
|
||||
x = xfwd(xr)
|
||||
if yscale == self.scale:
|
||||
yfwd = identity
|
||||
y = yr
|
||||
else:
|
||||
if self.scale == 'log':
|
||||
yfwd = from_scale[self.scale]
|
||||
else:
|
||||
yfwd = to_scale[yscale]
|
||||
y = yfwd(yr)
|
||||
|
||||
self.deviation = None
|
||||
nmin = min(nmin, nmax)
|
||||
n = len(x)
|
||||
relerror = yscale == 'lin'
|
||||
if len(x) > nmax:
|
||||
# reduce number of points, if needed
|
||||
i, j = 1, n - 1 # index range for calculating interpolation deviation
|
||||
deviation = np.zeros(n)
|
||||
while True:
|
||||
deviation[i:j] = self.interpolation_error(
|
||||
x[i-1:j-1], x[i+1:j+1], y[i-1:j-1], y[i+1:j+1],
|
||||
xbwd, yfwd, relerror)
|
||||
# calculate interpolation error when a single point is omitted
|
||||
if n <= nmax:
|
||||
break
|
||||
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
|
||||
y = np.delete(y, idx)
|
||||
x = np.delete(x, idx)
|
||||
deviation = np.delete(deviation, idx)
|
||||
n = len(x)
|
||||
# index range to recalculate
|
||||
i, j = max(1, idx - 1), min(n - 1, idx + 1)
|
||||
self.deviation = deviation # for debugging purposes
|
||||
elif n < nmin:
|
||||
if ibeg + 1 < iend:
|
||||
diff1, diff4 = self.interpolation_error(
|
||||
x[ibeg:iend - 1], x[ibeg + 1:iend], y[ibeg:iend - 1], y[ibeg + 1:iend],
|
||||
xbwd, yfwd, relerror, return_tuple=True)
|
||||
dif_target = 1e-4
|
||||
sq4 = np.sqrt(diff4) * 4
|
||||
sq1 = np.sqrt(diff1)
|
||||
offset = 0.49
|
||||
n_mid = nmax - len(x) + iend - ibeg - 1
|
||||
# iteration to find a dif target resulting in no more than nmax points
|
||||
while True:
|
||||
scale = 1 / np.sqrt(dif_target)
|
||||
# estimate number of intermediate points (float!) needed to reach dif_target
|
||||
# number of points estimated from the result of the interpolation error with 4 sections
|
||||
n4 = np.maximum(1, sq4 * scale)
|
||||
# number of points estimated from the result of the interpolation error with 1 section
|
||||
n1 = np.maximum(1, sq1 * scale)
|
||||
# use n4 where n4 > 4, n1, where n1 < 1 and a weighted average in between
|
||||
nn = np.select([n4 > 4, n1 > 1],
|
||||
[n4, (n4 * (n1 - 1) + n1 * (4 - n4)) / (3 + n1 - n4)], n1)
|
||||
n_tot = np.sum(np.rint(nn + offset))
|
||||
extra = n_tot - n_mid
|
||||
if extra <= 0:
|
||||
break
|
||||
dif_target *= (n_tot / n_mid) ** 2
|
||||
|
||||
xnew = [x[:ibeg]]
|
||||
for x0, x1, ni in zip(x[ibeg:iend-1], x[ibeg+1:iend], np.rint(nn + offset)):
|
||||
xnew.append(np.linspace(x0, x1, int(ni) + 1)[:-1])
|
||||
xnew.append(x[iend-1:])
|
||||
x = np.concatenate(xnew)
|
||||
y = yfwd(self.spline(xbwd(x)))
|
||||
# for debugging purposes:
|
||||
self.deviation = self.interpolation_error(x[:-1], x[1:], y[:-1], y[1:], xbwd, yfwd, relerror)
|
||||
|
||||
return np.stack([x, y], axis=1)
|
||||
|
144
frappy_psi/ccracks.py
Normal file
144
frappy_psi/ccracks.py
Normal file
@ -0,0 +1,144 @@
|
||||
import os
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
from configparser import ConfigParser
|
||||
from frappy.errors import ConfigError
|
||||
|
||||
|
||||
class Rack:
|
||||
configbase = Path('/home/l_samenv/.config/frappy_instruments')
|
||||
|
||||
def __init__(self, modfactory, **kwds):
|
||||
self.modfactory = modfactory
|
||||
instpath = self.configbase / os.environ['Instrument']
|
||||
sections = {}
|
||||
self.config = {}
|
||||
files = glob(str(instpath / '*.ini'))
|
||||
for filename in files:
|
||||
parser = ConfigParser()
|
||||
parser.optionxform = str
|
||||
parser.read([filename])
|
||||
for section in parser.sections():
|
||||
prev = sections.get(section)
|
||||
if prev:
|
||||
raise ConfigError(f'duplicate {section} section in {filename} and {prev}')
|
||||
sections[section] = filename
|
||||
self.config.update(parser.items(section))
|
||||
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>
|
||||
self.ccu_uri = {}
|
||||
|
||||
def set_props(self, mod, **kwds):
|
||||
for prop, method in kwds.items():
|
||||
value = self.props.get((prop, method))
|
||||
if value is None:
|
||||
# add mod to the list of cfgs to be fixed
|
||||
self.mods.setdefault((prop, method), []).append(mod)
|
||||
else:
|
||||
# set prop in current module
|
||||
if not mod.get(prop): # do not override given and not empty property
|
||||
mod[prop] = value
|
||||
|
||||
def fix_props(self, method, **kwds):
|
||||
for prop, value in kwds.items():
|
||||
if (prop, method) in self.props:
|
||||
raise ConfigError(f'duplicate call to {method}()')
|
||||
self.props[prop, method] = value
|
||||
# set property in modules to be fixed
|
||||
for mod in self.mods.get((prop, method), ()):
|
||||
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 = 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}',
|
||||
description='comm. to lakeshore in cc rack', uri=ls_uri)
|
||||
self.dev = Mod(dev, cls=f'frappy_psi.lakeshore.Device{self.ls_model}',
|
||||
description='lakeshore in cc rack', io=io, curve_handling=True)
|
||||
|
||||
def sensor(self, name, channel, calcurve, **kwds):
|
||||
Mod = self.modfactory
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.Sensor{self.ls_model}')
|
||||
kwds.setdefault('description', f'T sensor {name}')
|
||||
mod = Mod(name, channel=channel, calcurve=calcurve,
|
||||
device=self.ls_dev, **kwds)
|
||||
self.set_props(mod, io='lakeshore', dev='lakeshore')
|
||||
|
||||
def loop(self, name, channel, calcurve, output_module, **kwds):
|
||||
Mod = self.modfactory
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.Loop{self.ls_model}')
|
||||
kwds.setdefault('description', f'T loop {name}')
|
||||
Mod(name, channel=channel, calcurve=calcurve, output_module=output_module,
|
||||
device=self.ls_dev, **kwds)
|
||||
self.fix_props(f'heater({output_module})', description=f'heater for {name}')
|
||||
|
||||
def heater(self, name, output_no, max_heater, resistance, **kwds):
|
||||
Mod = self.modfactory
|
||||
if output_no == 1:
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.MainOutput{self.ls_model}')
|
||||
elif output_no == 2:
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.SecondaryOutput{self.ls_model}')
|
||||
else:
|
||||
return
|
||||
kwds.setdefault('description', '')
|
||||
mod = Mod(name, max_heater=max_heater, resistance=resistance, **kwds)
|
||||
self.set_props(mod, io='lakeshore', device='lakeshore', description=f'heater({name})')
|
||||
|
||||
def ccu(self, name=None, ccu_uri=None, ccu_io='ccu_io', args_for_io=None, **kwds):
|
||||
if args_for_io is None:
|
||||
args_for_io, kwds = kwds, {}
|
||||
prev_uri = self.ccu_uri.get(ccu_io)
|
||||
ccu_uri = ccu_uri or self.config.get('ccu_uri')
|
||||
if prev_uri:
|
||||
if prev_uri == ccu_uri:
|
||||
return kwds # already configured
|
||||
raise ConfigError(f'rack.{name or "ccu"}: ccu_uri {prev_uri} does not match {ccu_uri}')
|
||||
self.ccu_uri[ccu_io] = ccu_uri
|
||||
self.modfactory(ccu_io, 'frappy_psi.ccu4.IO', 'comm. to CCU4', uri=ccu_uri, **args_for_io)
|
||||
return kwds
|
||||
|
||||
def he(self, name='He_lev', ccu_io='ccu_io', **kwds):
|
||||
self.ccu('he', ccu_io=ccu_io, args_for_io={}, **kwds)
|
||||
self.modfactory(name, cls='frappy_psi.ccu4.HeLevel',
|
||||
description='the He Level', io=ccu_io, **kwds)
|
||||
|
||||
def n2(self, name='N2_lev', valve='N2_valve', upper='N2_upper', lower='N2_lower', ccu_io='ccu_io', **kwds):
|
||||
self.ccu('n2', ccu_io=ccu_io, args_for_io={}, **kwds)
|
||||
Mod = self.modfactory
|
||||
Mod(name, cls='frappy_psi.ccu4.N2Level',
|
||||
description='the N2 Level', io=ccu_io,
|
||||
valve=valve, upper=upper, lower=lower)
|
||||
Mod(valve, cls='frappy_psi.ccu4.N2FillValve',
|
||||
description='LN2 fill valve', io=ccu_io)
|
||||
Mod(upper, cls='frappy_psi.ccu4.N2TempSensor',
|
||||
description='upper LN2 sensor')
|
||||
Mod(lower, cls='frappy_psi.ccu4.N2TempSensor',
|
||||
description='lower LN2 sensor')
|
||||
|
||||
def flow(self, hepump_uri=None, hepump_type=None, hepump_io='hepump_io',
|
||||
hepump='hepump', hepump_mot='hepump_mot', hepump_valve='hepump_valve',
|
||||
flow_sensor='flow_sensor', pump_pressure='pump_pressure', nv='nv',
|
||||
ccu_io='ccu_io', **kwds):
|
||||
"""creates needle valve and pump access if available"""
|
||||
kwds = self.ccu('flow', ccu_io=ccu_io, args_for_io={}, **kwds)
|
||||
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, **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')
|
||||
return
|
||||
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, 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)
|
||||
|
@ -22,31 +22,33 @@
|
||||
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
||||
import time
|
||||
import math
|
||||
import numpy as np
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.lib import clamp, formatExtendedTraceback
|
||||
from frappy.lib.interpolation import Interpolation
|
||||
# the most common Frappy classes can be imported from frappy.core
|
||||
from frappy.core import HasIO, Parameter, Command, Readable, Writable, Drivable, \
|
||||
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached
|
||||
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached, nopoll
|
||||
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
|
||||
StatusType, IntRange, StringType, TupleOf
|
||||
from frappy.dynamic import Pinata
|
||||
StatusType, IntRange, StringType, TupleOf, ArrayOf
|
||||
from frappy.errors import CommunicationFailedError
|
||||
from frappy.states import HasStates, status_code, Retry
|
||||
|
||||
|
||||
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=5, no_motor=6)
|
||||
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=4, no_motor=5)
|
||||
A = Enum(disabled=0, manual=1, auto=2)
|
||||
|
||||
|
||||
class CCU4IO(StringIO):
|
||||
class IO(StringIO):
|
||||
"""communication with CCU4"""
|
||||
# for completeness: (not needed, as it is the default)
|
||||
end_of_line = '\n'
|
||||
# on connect, we send 'cid' and expect a reply starting with 'CCU4'
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
identification = [('cid', r'cid=CCU4.*')]
|
||||
|
||||
|
||||
class CCU4Base(HasIO):
|
||||
ioClass = CCU4IO
|
||||
class Base(HasIO):
|
||||
ioClass = IO
|
||||
|
||||
def command(self, **kwds):
|
||||
"""send a command and get the response
|
||||
@ -80,7 +82,7 @@ class CCU4Base(HasIO):
|
||||
return result
|
||||
|
||||
|
||||
class HeLevel(CCU4Base, Readable):
|
||||
class HeLevel(Base, Readable):
|
||||
"""He Level channel of CCU4"""
|
||||
|
||||
value = Parameter(unit='%')
|
||||
@ -122,10 +124,10 @@ class HeLevel(CCU4Base, Readable):
|
||||
return self.command(hfu=value)
|
||||
|
||||
|
||||
class Valve(CCU4Base, Writable):
|
||||
class Valve(Base, Writable):
|
||||
value = Parameter('relay state', BoolType())
|
||||
target = Parameter('relay target', BoolType())
|
||||
ioClass = CCU4IO
|
||||
ioClass = IO
|
||||
STATE_MAP = {0: (0, (IDLE, 'off')),
|
||||
1: (1, (IDLE, 'on')),
|
||||
2: (0, (ERROR, 'no valve')),
|
||||
@ -144,7 +146,7 @@ class Valve(CCU4Base, Writable):
|
||||
self.command(**self._close_command)
|
||||
|
||||
def read_status(self):
|
||||
state = self.command(self._query_state)
|
||||
state = int(self.command(**self._query_state))
|
||||
self.value, status = self.STATE_MAP[state]
|
||||
return status
|
||||
|
||||
@ -174,14 +176,14 @@ class N2TempSensor(Readable):
|
||||
value = Parameter('LN2 T sensor', FloatRange(unit='K'), default=0)
|
||||
|
||||
|
||||
class N2Level(CCU4Base, Pinata, Readable):
|
||||
class N2Level(Base, Readable):
|
||||
valve = Attached(Writable, mandatory=False)
|
||||
lower = Attached(Readable, mandatory=False)
|
||||
upper = Attached(Readable, mandatory=False)
|
||||
|
||||
value = Parameter('vessel state', EnumType(empty=0, ok=1, full=2))
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
mode = Parameter('auto mode', EnumType(A), readonly=False)
|
||||
status = Parameter(datatype=StatusType(Readable, 'DISABLED', 'BUSY'))
|
||||
mode = Parameter('auto mode', EnumType(A), readonly=False, default=A.manual)
|
||||
|
||||
threshold = Parameter('threshold triggering start/stop filling',
|
||||
FloatRange(unit='K'), readonly=False)
|
||||
@ -206,15 +208,6 @@ class N2Level(CCU4Base, Pinata, Readable):
|
||||
5: (WARN, 'empty'),
|
||||
}
|
||||
|
||||
def scanModules(self):
|
||||
for modname, name in self.names.items():
|
||||
if name:
|
||||
sensor_name = name.replace('$', self.name)
|
||||
self.setProperty(modname, sensor_name)
|
||||
yield sensor_name, {
|
||||
'cls': N2FillValve if modname == 'valve' else N2TempSensor,
|
||||
'description': f'LN2 {modname} T sensor'}
|
||||
|
||||
def initialReads(self):
|
||||
self.command(nav=1) # tell CCU4 to activate LN2 sensor readings
|
||||
super().initialReads()
|
||||
@ -280,202 +273,452 @@ class N2Level(CCU4Base, Pinata, Readable):
|
||||
|
||||
@Command()
|
||||
def fill(self):
|
||||
"""start filling"""
|
||||
self.mode = A.auto
|
||||
self.io.write(nc=1)
|
||||
self.command(nc=1)
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""stop filling"""
|
||||
if self.mode == A.auto:
|
||||
# set to watching
|
||||
self.command(nc=3)
|
||||
else:
|
||||
# set to off
|
||||
self.io.write(nc=0)
|
||||
self.command(nc=0)
|
||||
|
||||
|
||||
class FlowPressure(CCU4Base, Readable):
|
||||
class HasFilter:
|
||||
__value1 = None
|
||||
__value = None
|
||||
__last = None
|
||||
|
||||
def filter(self, filter_time, value):
|
||||
now = time.time()
|
||||
if self.__value is None:
|
||||
self.__last = now
|
||||
self.__value1 = value
|
||||
self.__value = value
|
||||
weight = (now - self.__last) / filter_time
|
||||
self.__value1 += weight * (value - self.__value)
|
||||
self.__value += weight * (self.__value1 - self.__value)
|
||||
self.__last = now
|
||||
return self.__value
|
||||
|
||||
|
||||
class Pressure(HasFilter, Base, Readable):
|
||||
value = Parameter(unit='mbar')
|
||||
mbar_offset = Parameter(unit='mbar', default=0.8, readonly=False)
|
||||
mbar_offset = Parameter('offset in mbar', FloatRange(unit='mbar'), default=0.8, readonly=False)
|
||||
filter_time = Parameter('filter time', FloatRange(unit='sec'), readonly=False, default=3)
|
||||
pollinterval = Parameter(default=0.25)
|
||||
|
||||
def read_value(self):
|
||||
return self.filter(self.command(f=float)) - self.mbar_offset
|
||||
return self.filter(self.filter_time, self.command(f=float)) - self.mbar_offset
|
||||
|
||||
|
||||
class NeedleValve(HasStates, CCU4Base, Drivable):
|
||||
flow = Attached(Readable, mandatory=False)
|
||||
flow_pressure = Attached(Readable, mandatory=False)
|
||||
def Table(miny=None, maxy=None):
|
||||
return ArrayOf(TupleOf(FloatRange(), FloatRange(miny, maxy)))
|
||||
|
||||
|
||||
class NeedleValveFlow(HasStates, Base, Drivable):
|
||||
flow_sensor = Attached(Readable, mandatory=False)
|
||||
pressure = Attached(Pressure, mandatory=False)
|
||||
use_pressure = Parameter('flag (use pressure instead of flow meter)', BoolType(),
|
||||
readonly=False, default=False)
|
||||
lnm_per_mbar = Parameter('scale factor', FloatRange(unit='lnm/mbar'), readonly=False, default=0.6)
|
||||
|
||||
value = Parameter(unit='ln/min')
|
||||
target = Parameter(unit='ln/min')
|
||||
|
||||
lnm_per_mbar = Parameter(unit='ln/min/mbar', default=0.6, readonly=False)
|
||||
use_pressure = Parameter('use flow from pressure', BoolType(),
|
||||
default=False, readonly=False)
|
||||
motor_state = Parameter('motor_state', EnumType(M))
|
||||
tolerance = Parameter('tolerance', FloatRange(0), value=0.25, readonly=False)
|
||||
tolerance2 = Parameter('tolerance limit above 2 lnm', FloatRange(0), value=0.5, readonly=False)
|
||||
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False)
|
||||
motor_state = Parameter('motor_state', EnumType(M), default=0)
|
||||
speed = Parameter('speed moving time / passed time', FloatRange())
|
||||
tolerance = Parameter('tolerance', Table(0), value=[(2,0.1),(4,0.4)], readonly=False)
|
||||
prop_open = Parameter('proportional term for opening', Table(0), readonly=False, value=[(1,0.05)])
|
||||
prop_close = Parameter('proportional term for closing', Table(0), readonly=False, value=[(1,0.02)])
|
||||
deriv = Parameter('min progress time constant', FloatRange(unit='s'),
|
||||
default=30, readonly=False)
|
||||
settle = Parameter('time within tolerance before getting quiet', FloatRange(unit='s'),
|
||||
default=30, readonly=False)
|
||||
step_factor = Parameter('factor (no progress time) / (min step size)', FloatRange(), default=300)
|
||||
control_active = Parameter('control active flag', BoolType(), readonly=False)
|
||||
pollinterval = Parameter(default=1)
|
||||
control_active = Parameter('control active flag', BoolType(), readonly=False, default=1)
|
||||
min_open_pulse = Parameter('minimal open step', FloatRange(0, unit='s'), readonly=False, default=0.02)
|
||||
min_close_pulse = Parameter('minimal close step', FloatRange(0, unit='s'), readonly=False, default=0.0)
|
||||
# raw_open_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.12)
|
||||
# raw_close_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.04)
|
||||
pollinterval = Parameter(datatype=FloatRange(1, unit='s'), default=5)
|
||||
_last_dirchange = 0
|
||||
_ref_time = 0
|
||||
_ref_dif = 0
|
||||
_last_cycle = 0
|
||||
_last_progress = 0
|
||||
_dir = 0
|
||||
_rawdir = 0
|
||||
_step = 0
|
||||
_speed_sum = 0
|
||||
_last_era = 0
|
||||
_value = None
|
||||
|
||||
def doPoll(self):
|
||||
# poll at least every sec, but update value only
|
||||
# every pollinterval and status when changed
|
||||
if not self.pollInfo.fast_flag:
|
||||
self.pollInfo.interval = min(1, self.pollinterval) # reduce internal poll interval
|
||||
self._value = self.get_value()
|
||||
self._last.append(self._value)
|
||||
del self._last[0:-300]
|
||||
self.read_motor_state()
|
||||
era = time.time() // self.pollinterval
|
||||
if era != self._last_era:
|
||||
self.speed = self._speed_sum / self.pollinterval
|
||||
self._speed_sum = 0
|
||||
self.value = self._value
|
||||
self._last_era = era
|
||||
self.read_status()
|
||||
self.cycle_machine()
|
||||
|
||||
def get_value(self):
|
||||
p = self.pressure.read_value() * self.lnm_per_mbar
|
||||
f = self.flow_sensor.read_value()
|
||||
return p if self.use_pressure else f
|
||||
|
||||
def initModule(self):
|
||||
self._last = []
|
||||
if self.pressure:
|
||||
self.pressure.addCallback('value', self.update_from_pressure)
|
||||
if self.flow_sensor:
|
||||
self.flow_sensor.addCallback('value', self.update_from_flow)
|
||||
super().initModule()
|
||||
if self.flow_pressure:
|
||||
self.flow_pressure.addCallback('value', self.update_flow_pressure)
|
||||
if self.flow:
|
||||
self.flow.addCallback('value', self.update_flow)
|
||||
self.write_tolerance(self.tolerance)
|
||||
|
||||
def write_tolerance(self, tolerance):
|
||||
if hasattr(self.flow_pressure, 'tolerance'):
|
||||
self.flow_pressure.tolerance = tolerance / self.lnm_per_mbar
|
||||
if hasattr(self.flow, 'tolerance'):
|
||||
self.flow.tolerance = tolerance
|
||||
def update_from_flow(self, value):
|
||||
if not self.use_pressure:
|
||||
self._value = value
|
||||
|
||||
def update_from_pressure(self, value):
|
||||
if self.use_pressure:
|
||||
self._value = value * self.lnm_per_mbar
|
||||
# self.cycle_machine()
|
||||
|
||||
def read_value(self):
|
||||
self._value = self.get_value()
|
||||
return self._value
|
||||
|
||||
def read_use_pressure(self):
|
||||
if self.flow_pressure:
|
||||
if self.flow:
|
||||
if self.pressure:
|
||||
if self.flow_sensor:
|
||||
return self.use_pressure
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_flow(self, value):
|
||||
if not self.use_pressure:
|
||||
self.value = value
|
||||
self.cycle_machine()
|
||||
|
||||
def update_flow_pressure(self, value):
|
||||
if self.use_pressure:
|
||||
self.value = value * self.lnm_per_mbar
|
||||
self.cycle_machine()
|
||||
|
||||
def write_target(self, value):
|
||||
self.start_machine(self.controlling, in_tol_time=0,
|
||||
ref_time=0, ref_dif=0, prev_dif=0)
|
||||
self.log.info('change target')
|
||||
self.target = value
|
||||
self.start_machine(self.change_target)
|
||||
|
||||
def write_prop_open(self, value):
|
||||
self._prop_open = Interpolation(value)
|
||||
return self._prop_open
|
||||
|
||||
def write_prop_close(self, value):
|
||||
self._prop_close = Interpolation(value)
|
||||
return self._prop_close
|
||||
|
||||
def write_tolerance(self, value):
|
||||
self._tolerance = Interpolation(value)
|
||||
return self._tolerance
|
||||
|
||||
@status_code(BUSY)
|
||||
def unblock_from_open(self, state):
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.motor_state == 'opened':
|
||||
self.command(mp=-60)
|
||||
return Retry
|
||||
if self.motor_state == 'closing':
|
||||
return Retry
|
||||
if self.motor_state == 'closed':
|
||||
if self.value > max(1, self.target):
|
||||
return Retry
|
||||
state.flow_before = self.value
|
||||
state.wiggle = 1
|
||||
state.start_wiggle = state.now
|
||||
self.command(mp=60)
|
||||
return self.unblock_open
|
||||
return self.approaching
|
||||
|
||||
@status_code(BUSY)
|
||||
def unblock_open(self, state):
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.value < state.flow_before:
|
||||
state.flow_before_open = self.value
|
||||
elif self.value > state.flow_before + 1:
|
||||
state.wiggle = -state.wiggle / 2
|
||||
self.command(mp=state.wiggle)
|
||||
state.start_wiggle = state.now
|
||||
return self.unblock_close
|
||||
if self.motor_state == 'opening':
|
||||
return Retry
|
||||
if self.motor_state == 'idle':
|
||||
self.command(mp=state.wiggle)
|
||||
return Retry
|
||||
if self.motor_state == 'opened':
|
||||
if state.now < state.start_wiggle + 20:
|
||||
return Retry
|
||||
return self.final_status(ERROR, 'can not open')
|
||||
def change_target(self, sm):
|
||||
sm.last_progress = sm.now
|
||||
sm.ref_time = 0
|
||||
sm.ref_dif = 0
|
||||
sm.last_pulse_time = 0
|
||||
sm.no_progress_pulse = (0.1, -0.05)
|
||||
self.log.info('target %s value %s', self.target, self._value)
|
||||
if abs(self.target - self._value) < self._tolerance(self._value):
|
||||
self.log.info('go to at_target')
|
||||
return self.at_target
|
||||
self.log.info('go to controlling')
|
||||
return self.controlling
|
||||
|
||||
@status_code(BUSY)
|
||||
def unblock_close(self, state):
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.value > state.flow_before:
|
||||
state.flow_before_open = self.value
|
||||
elif self.value < state.flow_before - 1:
|
||||
if state.wiggle < self.prop * 2:
|
||||
return self.final_status(IDLE, '')
|
||||
state.wiggle = -state.wiggle / 2
|
||||
self.command(mp=state.wiggle)
|
||||
state.start_wiggle = state.now
|
||||
return self.unblock_open
|
||||
if self.motor_state == 'closing':
|
||||
return Retry
|
||||
if self.motor_state == 'idle':
|
||||
self.command(mp=state.wiggle)
|
||||
return Retry
|
||||
if self.motor_state == 'closed':
|
||||
if state.now < state.start_wiggle + 20:
|
||||
return Retry
|
||||
return self.final_status(ERROR, 'can not close')
|
||||
return self.final_status(WARN, 'unblock interrupted')
|
||||
def filtered(self, n=60, m=5, nsigma=2):
|
||||
"""return mean and tolerance, augmented by noise"""
|
||||
# TODO: better idea: use median over last minute and last value and treat them both
|
||||
n = len(self._last[-n:])
|
||||
mean = np.median(self._last[-m:])
|
||||
tol = self._tolerance(mean)
|
||||
span = 0
|
||||
if len(self._last) >= n + m:
|
||||
# get span over the last n points
|
||||
span = max(self._last[-n:]) - min(self._last[-n:])
|
||||
slope = mean - np.median(self._last[-n-m:-n])
|
||||
# in case there is a slope, subtract it
|
||||
tol = math.sqrt(tol ** 2 + max(0, span-abs(slope)) ** 2)
|
||||
self.log.info('filt %d %d %d %g %g', len(self._last), n, m, self._value, span)
|
||||
m = min(m, n)
|
||||
narr = np.array(self._last[-n:])
|
||||
mdif = np.median(np.abs(narr[1:-1] - 0.5 * (narr[:-2] + narr[2:])))
|
||||
return mean, tol
|
||||
|
||||
def _tolerance(self):
|
||||
return min(self.tolerance * min(1, self.value / 2), self.tolerance2)
|
||||
@status_code(BUSY)
|
||||
def controlling(self, sm):
|
||||
tol = self._tolerance(self.target)
|
||||
dif = np.array([self.target - np.median(self._last[-m:]) for m in (1,5,60)])
|
||||
if sm.init:
|
||||
self.log.info('restart controlling')
|
||||
direction = math.copysign(1, dif[1])
|
||||
if direction != self._dir:
|
||||
self.log.info('new dir %g dif=%g', direction, dif[1])
|
||||
self._dir = direction
|
||||
self._last_dirchange = sm.now
|
||||
sm.ref_dif = abs(dif[1])
|
||||
sm.ref_time = sm.now
|
||||
difdir = dif * self._dir # negative when overshoot happend
|
||||
# difdif = dif - self._prev_dif
|
||||
# self._prev_dif = dif
|
||||
expected_dif = sm.ref_dif * math.exp((sm.ref_time - sm.now) / self.deriv)
|
||||
|
||||
if np.all(difdir < tol):
|
||||
if np.all(difdir < -tol):
|
||||
self.log.info('overshoot %r', dif)
|
||||
return self.controlling
|
||||
# within tolerance
|
||||
self.log.info('at target %r tol %g', dif, tol)
|
||||
return self.at_target
|
||||
if np.all(difdir > expected_dif):
|
||||
# not enough progress
|
||||
if sm.now > sm.last_progress + self.deriv:
|
||||
if sm.no_progress_pulse:
|
||||
pulse = abs(sm.no_progress_pulse[self._dir < 0]) * self._dir
|
||||
self.log.info('not enough progress %g', pulse)
|
||||
self.pulse(pulse)
|
||||
sm.last_progress = sm.now
|
||||
if sm.now < sm.last_pulse_time + 2.5:
|
||||
return Retry
|
||||
# TODO: check motor state for closed / opened ?
|
||||
difd = min(difdir[:2])
|
||||
sm.last_pulse_time = sm.now
|
||||
if self._dir > 0:
|
||||
minstep = self.min_open_pulse
|
||||
prop = self._prop_open(self._value)
|
||||
else:
|
||||
minstep = self.min_close_pulse
|
||||
prop = self._prop_close(self._value)
|
||||
if difd > 0:
|
||||
if prop * tol > minstep:
|
||||
# step outside tol is already minstep
|
||||
step = difd * prop
|
||||
else:
|
||||
if difd > tol:
|
||||
step = (minstep + (difd - tol) * prop)
|
||||
else:
|
||||
step = minstep * difd / tol
|
||||
step *= self._dir
|
||||
self.log.info('MP %g dif=%g tol=%g', step, difd * self._dir, tol)
|
||||
self.command(mp=step)
|
||||
self._speed_sum += step
|
||||
return Retry
|
||||
# still approaching
|
||||
difmax = max(difdir)
|
||||
if difmax < expected_dif:
|
||||
sm.ref_time = sm.now
|
||||
sm.ref_dif = difmax
|
||||
# self.log.info('new ref %g', sm.ref_dif)
|
||||
sm.last_progress = sm.now
|
||||
return Retry # progressing: no pulse needed
|
||||
|
||||
@status_code(IDLE)
|
||||
def at_target(self, state):
|
||||
dif = self.target - self.value
|
||||
if abs(dif) > self._tolerance():
|
||||
state.in_tol_time = 0
|
||||
def at_target(self, sm):
|
||||
tol = self._tolerance(self.target)
|
||||
dif = np.array([self.target - np.median(self._last[-m:]) for m in (1,5,60)])
|
||||
if np.all(dif > tol) or np.all(dif < -tol):
|
||||
return self.unstable
|
||||
return Retry
|
||||
|
||||
@status_code(IDLE, 'unstable')
|
||||
def unstable(self, state):
|
||||
return self.controlling(state)
|
||||
def unstable(self, sm):
|
||||
sm.no_progress_pulse = None
|
||||
return self.controlling(sm)
|
||||
|
||||
def read_motor_state(self):
|
||||
return self.command(fm=int)
|
||||
|
||||
@Command
|
||||
def close(self):
|
||||
"""close valve fully"""
|
||||
self.command(mp=-60)
|
||||
self.motor_state = self.command(fm=int)
|
||||
self.start_machine(self.closing, fast_poll=0.1)
|
||||
|
||||
@status_code(BUSY)
|
||||
def controlling(self, state):
|
||||
delta = state.delta(0)
|
||||
dif = self.target - self.value
|
||||
difdif = dif - state.prev_dif
|
||||
state.prev_dif = dif
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.motor_state == 'closed':
|
||||
if dif < 0 or difdif < 0:
|
||||
return Retry
|
||||
return self.unblock_from_open
|
||||
elif self.motor_state == 'opened': # trigger also when flow too high?
|
||||
if dif > 0 or difdif > 0:
|
||||
return Retry
|
||||
self.command(mp=-60)
|
||||
return self.unblock_from_open
|
||||
def closing(self, sm):
|
||||
if sm.init:
|
||||
sm.start_time = sm.now
|
||||
self._speed_sum -= sm.delta()
|
||||
self.read_motor_state()
|
||||
if self.motor_state == M.closing:
|
||||
return Retry
|
||||
if self.motor_state == M.closed:
|
||||
return self.final_status(IDLE, 'closed')
|
||||
if sm.now < sm.start_time + 1:
|
||||
return Retry
|
||||
return self.final_status(IDLE, 'fixed')
|
||||
|
||||
tolerance = self._tolerance()
|
||||
if abs(dif) < tolerance:
|
||||
state.in_tol_time += delta
|
||||
if state.in_tol_time > self.settle:
|
||||
return self.at_target
|
||||
@Command
|
||||
def open(self):
|
||||
"""open valve fully"""
|
||||
self.command(mp=60)
|
||||
self.read_motor_state()
|
||||
self.start_machine(self.opening, threshold=None)
|
||||
|
||||
@status_code(BUSY)
|
||||
def opening(self, sm):
|
||||
if sm.init:
|
||||
sm.start_time = sm.now
|
||||
self._speed_sum += sm.dleta()
|
||||
self.read_motor_state()
|
||||
if self.motor_state == M.opening:
|
||||
return Retry
|
||||
expected_dif = state.ref_dif * math.exp((state.now - state.ref_time) / self.deriv)
|
||||
if abs(dif) < expected_dif:
|
||||
if abs(dif) < expected_dif / 1.25:
|
||||
state.ref_time = state.now
|
||||
state.ref_dif = abs(dif) * 1.25
|
||||
state.last_progress = state.now
|
||||
return Retry # progress is fast enough
|
||||
state.ref_time = state.now
|
||||
state.ref_dif = abs(dif)
|
||||
state.step += dif * delta * self.prop
|
||||
if abs(state.step) < (state.now - state.last_progress) / self.step_factor:
|
||||
# wait until step size is big enough
|
||||
if self.motor_state == M.opened:
|
||||
return self.final_status(IDLE, 'opened')
|
||||
if sm.now < sm.start_time + 1:
|
||||
return Retry
|
||||
self.command(mp=state.step)
|
||||
return self.final_status(IDLE, 'fixed')
|
||||
|
||||
@Command
|
||||
def lim_pulse(self):
|
||||
"""try to open until pressure increases"""
|
||||
p = self.command(f=float)
|
||||
self.start_machine(self.lim_open, threshold=0.5,
|
||||
prev=[p], ref=p, fast_poll=0.1, cnt=0)
|
||||
|
||||
@status_code(BUSY)
|
||||
def lim_open(self, sm):
|
||||
self.read_motor_state()
|
||||
if self.motor_state == M.opening:
|
||||
return Retry
|
||||
if self.motor_state == M.opened:
|
||||
return self.final_status(IDLE, 'opened')
|
||||
press, measured = self.command(f=float, mmp=float)
|
||||
sm.prev.append(press)
|
||||
if press > sm.ref + 0.2:
|
||||
sm.cnt += 1
|
||||
if sm.cnt > 5 or press > sm.ref + 0.5:
|
||||
self.log.info('flow increased %g', press)
|
||||
return self.final_status(IDLE, 'flow increased')
|
||||
self.log.info('wait count %g', press)
|
||||
return Retry
|
||||
sm.cnt = 0
|
||||
last5 = sm.prev[-5:]
|
||||
median = sorted(last5)[len(last5) // 2]
|
||||
if press > median:
|
||||
# avoid to pulse again after an even small increase
|
||||
self.log.info('wait %g', press)
|
||||
return Retry
|
||||
sm.ref = min(sm.prev[0], median)
|
||||
if measured:
|
||||
self._speed_sum += measured
|
||||
if measured < 0.1:
|
||||
sm.threshold = round(sm.threshold * 1.1, 2)
|
||||
elif measured > 0.3:
|
||||
sm.threshold = round(sm.threshold * 0.9, 2)
|
||||
self.log.info('measured %g new threshold %g press %g', measured, sm.threshold, press)
|
||||
else:
|
||||
self._speed_sum += 1
|
||||
self.log.info('full pulse')
|
||||
sm.cnt = 0
|
||||
self.command(mft=sm.ref + sm.threshold, mp=1)
|
||||
return Retry
|
||||
|
||||
@Command(FloatRange())
|
||||
def pulse(self, value):
|
||||
"""perform a motor pulse"""
|
||||
self.command(mp=value)
|
||||
self._speed_sum += value
|
||||
if value > 0:
|
||||
self.motor_state = M.opening
|
||||
return self.opening
|
||||
self.motor_state = M.closing
|
||||
return self.closing
|
||||
|
||||
@Command()
|
||||
def autopar(self):
|
||||
"""adjust automatically needle valve parameters"""
|
||||
self.close()
|
||||
self.start_machine(self.auto_wait, open_pulse=0.1, close_pulse=0.05,
|
||||
minflow=self.read_value(), last=None)
|
||||
return self.auto_wait
|
||||
|
||||
def is_stable(self, sm, n, tol=0.01):
|
||||
"""wait for a stable flow
|
||||
|
||||
n: size of buffer
|
||||
tol: a tolerance
|
||||
"""
|
||||
if sm.last is None:
|
||||
sm.last = []
|
||||
sm.cnt = 0
|
||||
v = self.read_value()
|
||||
sm.last.append(v)
|
||||
del sm.last[:-n]
|
||||
dif = v - sm.last[0]
|
||||
if dif < -tol:
|
||||
sm.cnt -= 1
|
||||
elif dif > tol:
|
||||
sm.cnt += 1
|
||||
else:
|
||||
sm.cnt -= clamp(-1, sm.cnt, 1)
|
||||
if len(sm.last) < n:
|
||||
return False
|
||||
return abs(sm.cnt) < n // 2
|
||||
|
||||
def is_unstable(self, sm, n, tol=0.01):
|
||||
"""wait for a stable flow
|
||||
|
||||
return 0, -1 or 1
|
||||
"""
|
||||
if sm.last is None:
|
||||
sm.last = []
|
||||
sm.cnt = 0
|
||||
v = self.read_value()
|
||||
prevmax = max(sm.last)
|
||||
prevmin = min(sm.last)
|
||||
sm.last.append(v)
|
||||
del sm.last[:-n]
|
||||
self.log.info('unstable %g >? %g <? %g', v, prevmax, prevmin)
|
||||
if v > prevmax + tol:
|
||||
return 1
|
||||
if v < prevmin - tol:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
@status_code(BUSY)
|
||||
def auto_wait(self, sm):
|
||||
stable = self.is_stable(sm, 5, 0.01)
|
||||
if self._value < sm.minflow:
|
||||
sm.minflow = self._value
|
||||
if self.read_motor_state() == M.closing or not stable:
|
||||
return Retry
|
||||
return self.auto_open
|
||||
|
||||
@status_code(BUSY)
|
||||
def auto_open(self, sm):
|
||||
stable = self.is_unstable(sm, 5, 0.1)
|
||||
if stable > 0:
|
||||
sm.start_time = sm.now
|
||||
sm.flow_before = sm.last[-1]
|
||||
self.pulse(sm.open_pulse)
|
||||
return self.auto_close
|
||||
if sm.delta(sm.open_pulse * 2) is not None:
|
||||
self.pulse(sm.open_pulse)
|
||||
return Retry
|
||||
|
||||
@status_code(BUSY)
|
||||
def auto_open_stable(self, sm):
|
||||
if self.is_stable(sm, 5, 0.01):
|
||||
return Retry
|
||||
return self.auto_close
|
||||
|
||||
@status_code(BUSY)
|
||||
def auto_close(self, sm):
|
||||
if not self.is_stable(sm, 10, 0.01):
|
||||
return Retry
|
||||
self.log.info('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, '')
|
||||
|
||||
|
300
frappy_psi/dilution_statemachine.py
Normal file
300
frappy_psi/dilution_statemachine.py
Normal file
@ -0,0 +1,300 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.core import Drivable, Parameter, EnumType, Attached, FloatRange, \
|
||||
Command, IDLE, BUSY, WARN, ERROR, Property
|
||||
from frappy.datatypes import StatusType, EnumType, ArrayOf, BoolType, IntRange
|
||||
from frappy.states import StateMachine, Retry, Finish, status_code, HasStates
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.errors import ImpossibleError
|
||||
import time
|
||||
Targetstates = Enum(
|
||||
SORBPUMP = 0,
|
||||
CONDENSE = 1,
|
||||
CIRCULATE = 2,
|
||||
REMOVE = 3,
|
||||
MANUAL = 4,
|
||||
TEST = 5,
|
||||
STOP = 6,
|
||||
)
|
||||
|
||||
class Dilution(HasStates, Drivable):
|
||||
|
||||
condenseline_pressure = Attached()
|
||||
condense_valve = Attached()
|
||||
dump_valve = Attached()
|
||||
|
||||
circulate_pump = Attached()
|
||||
compressor = Attached(mandatory=(False))
|
||||
turbopump = Attached(mandatory=(False))
|
||||
condenseline_valve = Attached()
|
||||
circuitshort_valve = Attached()
|
||||
still_pressure = Attached()
|
||||
#ls372 = Attached()
|
||||
V5 = Attached() #Name noch ändern!!!
|
||||
p1 = Attached() #Name noch ändern!!!
|
||||
|
||||
condensing_p_low = Property('Lower limit for condenseline pressure', IntRange())
|
||||
|
||||
condensing_p_high = Property('Lower limit for condenseline pressure', IntRange())
|
||||
|
||||
target = Parameter('target state', EnumType(Targetstates))
|
||||
|
||||
value = Parameter('target state', EnumType(Targetstates))
|
||||
|
||||
init = True
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
|
||||
def read_value(self):
|
||||
return self.value
|
||||
|
||||
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
|
||||
"""
|
||||
if (target == Targetstates.TEST):
|
||||
self.value = Targetstates.TEST
|
||||
self.init = True
|
||||
self.start_machine(self.test)
|
||||
|
||||
if (target == Targetstates.REMOVE):
|
||||
if self.value == target:
|
||||
return target
|
||||
if self.value != Teststates.CIRCULATE:
|
||||
self.final_status(WARN, "state before is not circulate")
|
||||
return self.value
|
||||
self.value = Targetstates.REMOVE
|
||||
self.init = True
|
||||
self.start_machine(self.remove)
|
||||
|
||||
elif (target == Targetstates.CIRCULATE):
|
||||
if self.value == target:
|
||||
return target
|
||||
self.value = Targetstates.CIRCULATE
|
||||
self.init = True
|
||||
self.start_machine(self.circulate)
|
||||
|
||||
elif (target == Targetstates.CONDENSE):
|
||||
if self.value == target:
|
||||
return target
|
||||
self.value = Targetstates.CONDENSE
|
||||
self.init = True
|
||||
self.start_machine(self.condense)
|
||||
|
||||
elif(target == Targetstates.MANUAL):
|
||||
self.value = Targetstates.MANUAL
|
||||
self.stop_machine()
|
||||
|
||||
elif (target == Targetstates.STOP):
|
||||
self.value = Targetstates.STOP
|
||||
self.stop_machine()
|
||||
return self.value
|
||||
|
||||
"""
|
||||
@status_code(BUSY, 'sorbpump state')
|
||||
def sorbpump(self, state):
|
||||
#Heizt Tsorb auf und wartet ab.
|
||||
if self.init:
|
||||
self.ls372.write_target(40) #Setze Tsorb auf 40K
|
||||
self.start_time = self.now
|
||||
self.init = false
|
||||
return Retry
|
||||
|
||||
if self.now - self.start_time < 2400: # 40 Minuten warten
|
||||
return Retry
|
||||
|
||||
self.ls372.write_target(0)
|
||||
|
||||
if self.ls372.read_value() > 10: # Warten bis Tsorb unter 10K
|
||||
return Retry
|
||||
|
||||
return self.condense
|
||||
"""
|
||||
|
||||
@status_code(BUSY, 'test mode')
|
||||
def test(self, state):
|
||||
"Nur zum testen, ob UI funktioniert"
|
||||
self.init = False
|
||||
self.condense_valve.write_target(1)
|
||||
time.sleep(1)
|
||||
self.condense_valve.write_target(0)
|
||||
self.dump_valve.write_target(1)
|
||||
time.sleep(1)
|
||||
self.dump_valve.write_target(0)
|
||||
self.compressor.write_target(1)
|
||||
return True
|
||||
|
||||
@status_code(BUSY, 'condense mode')
|
||||
def wait_for_condense_line_pressure(self, state):
|
||||
if (self.condenseline_pressure.read_value > 500):
|
||||
return Retry
|
||||
|
||||
return self.circulate
|
||||
|
||||
|
||||
def initialize_condense_valves(self):
|
||||
return True
|
||||
|
||||
@status_code(BUSY, 'condense state')
|
||||
def condense(self, state):
|
||||
"""Führt das Kondensationsverfahren durch."""
|
||||
if self.init:
|
||||
self.initialize_condense_valves()
|
||||
self.circuitshort_valve.write_target(0)
|
||||
self.dump_valve.write_target(0)
|
||||
self.condense_valve.write_target(0)
|
||||
|
||||
self.condenseline_valve.write_target(1)
|
||||
self.V5.write_target(1)
|
||||
|
||||
if (self.compressor is not None):
|
||||
self.compressor.write_target(1)
|
||||
|
||||
self.circulate_pump.write_target(1)
|
||||
self.init = False
|
||||
return Retry
|
||||
|
||||
if self.condenseline_pressure.read_value() < self.condensing_p_low:
|
||||
self.condense_valve.write_target(1)
|
||||
elif (self.condenseline_pressure.read_value() > self.condensing_p_high):
|
||||
self.condense_valve.write_target(0)
|
||||
|
||||
if (self.p1.read_value() > 20):
|
||||
return Retry
|
||||
|
||||
self.condense_valve.write_target(1)
|
||||
|
||||
if (self.turbopump is not None):
|
||||
if (self.condenseline_pressure.read_value() > 900 and self.still_pressure.read_value() > 10):
|
||||
return Retry
|
||||
else:
|
||||
self.turbopump.write_target(1)
|
||||
|
||||
return self.wait_for_condense_line_pressure
|
||||
|
||||
|
||||
def initialize_circulation_valves(self):
|
||||
return True
|
||||
|
||||
@status_code(BUSY, 'circulate state')
|
||||
def circulate(self):
|
||||
"""Zirkuliert die Mischung."""
|
||||
return self.initialize_circulation_valves()
|
||||
|
||||
|
||||
@status_code(BUSY, 'remove state')
|
||||
def remove(self):
|
||||
"""Entfernt die Mischung."""
|
||||
|
||||
if self.init:
|
||||
self.condenseline_valve.write_target(0)
|
||||
self.dump_valve.write_target(1)
|
||||
self.start_time = self.now
|
||||
self.init = False
|
||||
return Retry
|
||||
|
||||
if self.turbopump is not None:
|
||||
self.turbopump.write_target(0)
|
||||
|
||||
if (self.now - self.start_time < 300 or self.turbopump.read_speed() > 60):
|
||||
return Retry
|
||||
|
||||
self.circuitshort_valve.write_target(1)
|
||||
|
||||
if self.turbopump is not None:
|
||||
if self.still_pressure.read_value() > 20:
|
||||
return Retry
|
||||
self.turbopump.write_target(1)
|
||||
|
||||
if self.still_pressure.read_value() > 1e-4:
|
||||
return Retry
|
||||
|
||||
self.circuitshort_valve.write_target(0)
|
||||
self.dump_valve.write_target(0)
|
||||
|
||||
if self.compressor is not None:
|
||||
self.compressor.write_target(0)
|
||||
|
||||
for valve in self.remove_closed_valves:
|
||||
valve.write_target(0)
|
||||
|
||||
self.circulate_pump.write_target(0)
|
||||
|
||||
return Finish
|
||||
|
||||
class DIL5(Dilution):
|
||||
|
||||
MV10 = Attached()
|
||||
MV13 = Attached()
|
||||
MV8 = Attached()
|
||||
MVB = Attached()
|
||||
MV2 = Attached()
|
||||
MV1 = Attached()
|
||||
MV3a = Attached()
|
||||
MV3b = Attached()
|
||||
GV1 = Attached()
|
||||
MV14 = Attached()
|
||||
MV12 = Attached()
|
||||
MV11 = Attached()
|
||||
MV9 = Attached()
|
||||
GV2 = Attached()
|
||||
|
||||
def earlyInit(self):
|
||||
self.circulate_closed_valves = [self.condense_valve, self.dump_valve, self.circuitshort_valve, self.MV10, self.MV13, self.MV8, self.MVB, self.MV2]
|
||||
self.circulate_open_valves = [self.MV11, self.circulate_pump, self.GV2, self.V5, self.compressor, self.condenseline_valve, self.MV1, self.MV3a, self.MV3b, self.GV1, self.MV9, self.MV14]
|
||||
self.condense_closed_valves = [self.MV10, self.MV13, self.MV8, self.MVB, self.MV2]
|
||||
self.condense_open_valves = [self.MV1, self.MV3a, self.MV3b, self.GV1, self.MV9, self.MV14, self.MV12, self.MV11]
|
||||
super().earlyInit()
|
||||
|
||||
def initialize_condense_valves(self):
|
||||
#Anfangszustand der Ventile überprüfen
|
||||
for valve in self.condense_open_valves:
|
||||
if valve.read_value() == 0:
|
||||
self.stop_machine()
|
||||
raise ImpossibleError(f'valve {valve.name} must be open')
|
||||
|
||||
for valve in self.condense_closed_valves:
|
||||
if valve.read_value == 1:
|
||||
self.stop_machine()
|
||||
return ImpossibleError(f'valve {valve.name} must be closed')
|
||||
|
||||
def initialize_circulation_valves(self):
|
||||
#Anfangszustand der Ventile überprüfen
|
||||
self.value = Targetstates.CIRCULATE
|
||||
for valve in self.circulate_closed_valves:
|
||||
if (valve.read_value() == 1):
|
||||
self.stop_machine()
|
||||
raise ImpossibleError(f'valve {valve.name} must be open')
|
||||
|
||||
for valve in self.circulate_open_valves:
|
||||
if (valve.read_value() == 0):
|
||||
valve.write_target(1)
|
||||
self.stop_machine()
|
||||
raise ImpossibleError(f'valve {valve.name} must be open')
|
||||
|
||||
|
@ -56,7 +56,7 @@ class Drums(Writable):
|
||||
self._pos = 0
|
||||
for i, action in enumerate(self.pattern[self._pos:]):
|
||||
upper = action.upper()
|
||||
relais = self.actions.get(action.upper())
|
||||
relais = self.actions.get(upper)
|
||||
if relais:
|
||||
relais.write_target(upper == action) # True when capital letter
|
||||
else:
|
||||
|
@ -17,41 +17,104 @@
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
"""interlocks for furnance"""
|
||||
"""interlocks for furnace"""
|
||||
|
||||
import time
|
||||
from frappy.core import Module, Writable, Attached, Parameter, FloatRange, Readable,\
|
||||
BoolType, ERROR, IDLE
|
||||
from frappy.errors import ImpossibleError
|
||||
from frappy.mixins import HasControlledBy
|
||||
from frappy_psi.picontrol import PImixin
|
||||
from frappy_psi.convergence import HasConvergence
|
||||
from frappy_psi.ionopimax import CurrentInput, LogVoltageInput
|
||||
import frappy_psi.tdkpower as tdkpower
|
||||
import frappy_psi.bkpower as bkpower
|
||||
|
||||
|
||||
class Interlocks(Module):
|
||||
input = Attached(Readable, 'the input module')
|
||||
vacuum = Attached (Readable, 'the vacuum pressure')
|
||||
wall_T = Attached (Readable, 'the wall temperature')
|
||||
class Interlocks(Writable):
|
||||
value = Parameter('interlock o.k.', BoolType(), default=True)
|
||||
target = Parameter('set to true to confirm', BoolType(), readonly=False)
|
||||
input = Attached(Readable, 'the input module', mandatory=False) # TODO: remove
|
||||
vacuum = Attached(Readable, 'the vacuum pressure', mandatory=False)
|
||||
wall_T = Attached(Readable, 'the wall temperature', mandatory=False)
|
||||
htr_T = Attached(Readable, 'the heater temperature', mandatory=False)
|
||||
main_T = Attached(Readable, 'the main temperature')
|
||||
extra_T = Attached(Readable, 'the extra temperature')
|
||||
control = Attached(Module, 'the control module')
|
||||
relais = Attached(Writable, 'the interlock relais')
|
||||
htr = Attached(Module, 'the heater module', mandatory=False)
|
||||
relais = Attached(Writable, 'the interlock relais', mandatory=False)
|
||||
flowswitch = Attached(Readable, 'the flow switch', mandatory=False)
|
||||
wall_limit = Parameter('maximum wall temperature', FloatRange(0, unit='degC'),
|
||||
default = 50, readonly = False)
|
||||
vacuum_limit = Parameter('maximum vacuum pressure', FloatRange(0, unit='mbar'),
|
||||
default = 0.1, readonly = False)
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
if self.input.status[0] >= ERROR:
|
||||
self.control.status = self.input.status
|
||||
elif self.vacuum.value > self.vacuum_limit:
|
||||
self.control.status = ERROR, 'bad vacuum'
|
||||
elif self.wall_T.value > self.wall_limit:
|
||||
self.control.status = ERROR, 'wall overheat'
|
||||
else:
|
||||
return
|
||||
self.control.write_control_active(False)
|
||||
self.relais.write_target(False)
|
||||
htr_T_limit = Parameter('maximum htr temperature', FloatRange(0, unit='degC'),
|
||||
default = 530, readonly = False)
|
||||
main_T_limit = Parameter('maximum main temperature', FloatRange(0, unit='degC'),
|
||||
default = 530, readonly = False)
|
||||
extra_T_limit = Parameter('maximum extra temperature', FloatRange(0, unit='degC'),
|
||||
default = 530, readonly = False)
|
||||
|
||||
_off_reason = None # reason triggering interlock
|
||||
_conditions = '' # summary of reasons why locked now
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._sensor_checks = [
|
||||
(self.wall_T, 'wall_limit'),
|
||||
(self.main_T, 'main_T_limit'),
|
||||
(self.extra_T, 'extra_T_limit'),
|
||||
(self.htr_T, 'htr_T_limit'),
|
||||
(self.vacuum, 'vacuum_limit'),
|
||||
]
|
||||
|
||||
def write_target(self, value):
|
||||
if value:
|
||||
self.read_status()
|
||||
if self._conditions:
|
||||
raise ImpossibleError('not ready to start')
|
||||
self._off_reason = None
|
||||
self.value = True
|
||||
elif self.value:
|
||||
self.switch_off()
|
||||
self._off_reason = 'switched off'
|
||||
self.value = False
|
||||
self.read_status()
|
||||
|
||||
def switch_off(self):
|
||||
if self.value:
|
||||
self._off_reason = self._conditions
|
||||
self.value = False
|
||||
if self.control.control_active:
|
||||
self.log.error('switch control off %r', self.control.status)
|
||||
self.control.write_control_active(False)
|
||||
self.control.status = ERROR, self._conditions
|
||||
if self.htr and self.htr.target:
|
||||
self.htr.write_target(0)
|
||||
if self.relais and (self.relais.value or self.relais.target):
|
||||
self.relais.write_target(False)
|
||||
|
||||
def read_status(self):
|
||||
conditions = []
|
||||
if self.flowswitch and self.flowswitch.value == 0:
|
||||
conditions.append('no cooling water')
|
||||
for sensor, limitname in self._sensor_checks:
|
||||
if sensor is None:
|
||||
continue
|
||||
if sensor.value > getattr(self, limitname):
|
||||
conditions.append(f'above {sensor.name} limit')
|
||||
if sensor.status[0] >= ERROR:
|
||||
conditions.append(f'error at {sensor.name}: {sensor.status[1]}')
|
||||
break
|
||||
self._conditions = ', '.join(conditions)
|
||||
if conditions and (self.control.control_active or self.htr.target):
|
||||
self.switch_off()
|
||||
if self.value:
|
||||
return IDLE, '; '.join(conditions)
|
||||
return ERROR, self._off_reason
|
||||
|
||||
|
||||
class PI(PImixin, Writable):
|
||||
input = Attached(Readable, 'the input module')
|
||||
class PI(HasConvergence, PImixin):
|
||||
input_module = Attached(Readable, 'the input module')
|
||||
relais = Attached(Writable, 'the interlock relais', mandatory=False)
|
||||
|
||||
def read_value(self):
|
||||
@ -61,3 +124,23 @@ class PI(PImixin, Writable):
|
||||
super().write_target(value)
|
||||
if self.relais:
|
||||
self.relais.write_target(1)
|
||||
|
||||
|
||||
class TdkOutput(HasControlledBy, tdkpower.Output):
|
||||
pass
|
||||
|
||||
|
||||
class BkOutput(HasControlledBy, bkpower.Output):
|
||||
pass
|
||||
|
||||
|
||||
class PRtransmitter(CurrentInput):
|
||||
rawrange = (0.004, 0.02)
|
||||
extendedrange = (0.0036, 0.021)
|
||||
|
||||
|
||||
class PKRgauge(LogVoltageInput):
|
||||
rawrange = (1.82, 8.6)
|
||||
valuerange = (5e-9, 1000)
|
||||
extendedrange = (0.5, 9.5)
|
||||
value = Parameter(unit='mbar')
|
||||
|
71
frappy_psi/hepump.py
Normal file
71
frappy_psi/hepump.py
Normal file
@ -0,0 +1,71 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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 BoolType, FloatRange, Parameter, Readable, Writable, Attached, EnumType, nopoll
|
||||
from frappy_psi.trinamic import Motor
|
||||
from frappy_psi.ccu4 import Pressure, NeedleValveFlow
|
||||
|
||||
|
||||
class ValveMotor(Motor):
|
||||
has_inputs = True
|
||||
|
||||
|
||||
class HePump(Writable):
|
||||
valvemotor = Attached(Motor)
|
||||
flow = Attached(NeedleValveFlow)
|
||||
valve = Attached(Writable)
|
||||
value = Parameter(datatype=BoolType())
|
||||
target = Parameter(datatype=BoolType())
|
||||
pump_type = Parameter('pump type', EnumType(no=0, neodry=1, xds35=2, sv65=3), readonly=False, default=0)
|
||||
eco_mode = Parameter('eco mode', BoolType(), readonly=False)
|
||||
has_feedback = Parameter('feedback works', BoolType(), readonly=False, default=True)
|
||||
|
||||
FLOW_SCALE = {'no': 0, 'neodry': 0.55, 'xds35': 0.6, 'sv65': 0.9}
|
||||
|
||||
def write_target(self, value):
|
||||
self.valvemotor.write_output0(value)
|
||||
|
||||
def read_target(self):
|
||||
return self.valvemotor.read_output0()
|
||||
|
||||
def read_value(self):
|
||||
if self.has_feedback:
|
||||
return not self.valvemotor.read_input3()
|
||||
return self.target
|
||||
|
||||
def write_pump_type(self, value):
|
||||
self.flow.pressure_scale = self.FLOW_SCALE[value.name]
|
||||
|
||||
def read_eco_mode(self):
|
||||
if self.pump_type == 'xds35':
|
||||
return self.valvemotor.read_output1()
|
||||
return False
|
||||
|
||||
def write_eco_mode(self, value):
|
||||
if self.pump_type == 'xds35':
|
||||
return self.valvemotor.write_output1(value)
|
||||
# else silently ignore
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -18,55 +18,117 @@
|
||||
# Jael Celia Lorenzana <jael-celia.lorenzana@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.core import Readable, Writable, Parameter, BoolType, StringType,\
|
||||
FloatRange, Property, TupleOf, ERROR, IDLE
|
||||
"""support for iono pi max from Sfera Labs
|
||||
|
||||
supports also the smaller model iono pi
|
||||
"""
|
||||
|
||||
from math import log
|
||||
from pathlib import Path
|
||||
from frappy.core import Readable, Writable, Parameter, Property, ERROR, IDLE, WARN
|
||||
from frappy.errors import ConfigError, OutOfRangeError, ProgrammingError
|
||||
from frappy.datatypes import BoolType, EnumType, FloatRange, NoneOr, StringType, TupleOf
|
||||
|
||||
|
||||
class Base:
|
||||
addr = Property('address', StringType())
|
||||
_devpath = None
|
||||
_devclass = None
|
||||
_status = IDLE, ''
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.log.info('initModule %r', self.name)
|
||||
candidates = list(Path('/sys/class').glob(f'ionopi*/*/{self.addr}'))
|
||||
if not candidates:
|
||||
raise ConfigError(f'can not find path for {self.addr}')
|
||||
if len(candidates) > 1:
|
||||
raise ProgrammingError(f"ambiguous paths {','.join(candidates)}")
|
||||
self._devpath = candidates[0].parent
|
||||
self._devclass = candidates[0].parent.name
|
||||
|
||||
def read(self, addr, scale=None):
|
||||
with open(f'/sys/class/ionopimax/{self.devclass}/{addr}') as f:
|
||||
with open(self._devpath / addr) as f:
|
||||
result = f.read()
|
||||
if scale:
|
||||
return float(result) / scale
|
||||
return result
|
||||
return result.strip()
|
||||
|
||||
def write(self, addr, value, scale=None):
|
||||
value = str(round(value * scale)) if scale else str(value)
|
||||
with open(f'/sys/class/ionopimax/{self.devclass}/{addr}', 'w') as f:
|
||||
with open(self._devpath / addr, 'w') as f:
|
||||
f.write(value)
|
||||
|
||||
def read_status(self):
|
||||
return self._status
|
||||
|
||||
|
||||
class DigitalInput(Base, Readable):
|
||||
value = Parameter('input state', BoolType())
|
||||
devclass = 'digital_in'
|
||||
true_level = Property('level representing True', EnumType(low=0, high=1), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
if self._devclass == 'digital_io':
|
||||
self.write(f'{self.addr}_mode', 'inp')
|
||||
|
||||
def read_value(self):
|
||||
return self.read(self.addr, 1)
|
||||
return self.read(self.addr, 1) == self.true_level
|
||||
|
||||
|
||||
class DigitalOutput(DigitalInput, Writable):
|
||||
target = Parameter('output state', BoolType(), readonly=False)
|
||||
devclass = 'digital_out'
|
||||
|
||||
def read_value(self):
|
||||
reply = self.read(self.addr)
|
||||
try:
|
||||
self._status = IDLE, ''
|
||||
value = int(reply)
|
||||
except ValueError:
|
||||
if reply == 'S':
|
||||
if self.addr.startswith('oc'):
|
||||
self._status = ERROR, 'short circuit'
|
||||
else:
|
||||
self._status = ERROR, 'fault while closed'
|
||||
value = 0
|
||||
else:
|
||||
self._status = ERROR, 'fault while open'
|
||||
value = 1
|
||||
self.read_status()
|
||||
return value == self.true_level
|
||||
|
||||
def write_target(self, value):
|
||||
self.write(self.addr, value, 1)
|
||||
self.write(self.addr, value == self.true_level, 1)
|
||||
|
||||
|
||||
class AnalogInput(Base, Readable):
|
||||
value = Parameter('analog value', FloatRange())
|
||||
rawrange = Property('raw range(electronic)', TupleOf(FloatRange(),FloatRange()))
|
||||
valuerange = Property('value range(physical)', TupleOf(FloatRange(),FloatRange()))
|
||||
devclass = 'analog_in'
|
||||
rawrange = Property('raw range (electronic)', TupleOf(FloatRange(),FloatRange()))
|
||||
valuerange = Property('value range (physical)', TupleOf(FloatRange(),FloatRange()))
|
||||
extendedrange = Property('range outside calibrated range, but not sensor fault',
|
||||
NoneOr(TupleOf(FloatRange(), FloatRange())), default=None)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
dt = self.parameters['value'].datatype
|
||||
dt.min, dt.max = self.valuerange
|
||||
|
||||
def read_value(self):
|
||||
x0, x1 = self.rawrange
|
||||
y0, y1 = self.valuerange
|
||||
self.x = self.read(self.addr, self.scale)
|
||||
self.read_status()
|
||||
if self.status[0] == ERROR:
|
||||
raise OutOfRangeError('sensor fault')
|
||||
return y0 + (y1 - y0) * (self.x - x0) / (x1 - x0)
|
||||
|
||||
def read_status(self):
|
||||
if self.rawrange[0] <= self.x <= self.rawrange[1]:
|
||||
return IDLE, ''
|
||||
if self.extendedrange is None or self.extendedrange[0] <= self.x <= self.extendedrange[1]:
|
||||
return WARN, 'out of range'
|
||||
return ERROR, 'sensor fault'
|
||||
|
||||
|
||||
class VoltageInput(AnalogInput):
|
||||
scale = 1e5
|
||||
@ -82,30 +144,24 @@ class LogVoltageInput(VoltageInput):
|
||||
x0, x1 = self.rawrange
|
||||
y0, y1 = self.valuerange
|
||||
self.x = self.read(self.addr, self.scale)
|
||||
a = (x1-x0)/log(y1/y0,10)
|
||||
self.read_status()
|
||||
if self.status[0] == ERROR:
|
||||
raise OutOfRangeError('sensor fault')
|
||||
a = (x1-x0)/log(y1/y0, 10)
|
||||
return 10**((self.x-x1)/a)*y1
|
||||
|
||||
|
||||
class CurrentInput(AnalogInput):
|
||||
scale = 1e6
|
||||
rawrange = (0.004,0.02)
|
||||
rawrange = (0.004, 0.02)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.write(f'{self.addr}_mode','U')
|
||||
|
||||
def read_value(self):
|
||||
result = super().read_value()
|
||||
if self.x > 0.021:
|
||||
self.status = ERROR, 'sensor broken'
|
||||
else:
|
||||
self.status = IDLE, ''
|
||||
return result
|
||||
self.write(f'{self.addr}_mode', 'U')
|
||||
|
||||
|
||||
class AnalogOutput(AnalogInput, Writable):
|
||||
target = Parameter('outputvalue', FloatRange())
|
||||
devclass = 'analog_out'
|
||||
|
||||
def write_target(self, value):
|
||||
x0, x1 = self.rawrange
|
||||
@ -123,3 +179,18 @@ class VoltageOutput(AnalogOutput):
|
||||
self.write(f'{self.addr}_mode', 'V')
|
||||
self.write(f'{self.addr}', '0')
|
||||
self.write(f'{self.addr}_enabled', '1')
|
||||
|
||||
|
||||
class VoltagePower(Base, Writable):
|
||||
target = Parameter(datatype=FloatRange(0, 24.5, unit='V'), default=12)
|
||||
addr = 'vso'
|
||||
|
||||
def write_target(self, value):
|
||||
if value:
|
||||
self.log.info('write vso %r', value)
|
||||
self.write(self.addr, value, 1000)
|
||||
self.write(f'{self.addr}_enabled', 1)
|
||||
else:
|
||||
self.write(f'{self.addr}_enabled', 0)
|
||||
|
||||
|
||||
|
@ -1,16 +1,65 @@
|
||||
"""
|
||||
Created on Tue Feb 4 11:07:56 2020
|
||||
|
||||
@author: tartarotti_d-adm
|
||||
"""
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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:
|
||||
# Damaris Tartarotti Maimone
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""support for ultrasound plot clients"""
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
# disable the behaviour of raising the window to the front each time it is updated
|
||||
plt.rcParams["figure.raise_window"] = False
|
||||
|
||||
|
||||
NAN = float('nan')
|
||||
|
||||
|
||||
class Pause:
|
||||
"""allows to leave the plot loop when the window is closed
|
||||
|
||||
Usage:
|
||||
pause = Pause(fig)
|
||||
|
||||
# do initial plots
|
||||
plt.show()
|
||||
while pause(0.5):
|
||||
# do plot updates
|
||||
plt.draw()
|
||||
"""
|
||||
def __init__(self, fig):
|
||||
fig.canvas.mpl_connect('close_event', self.on_close)
|
||||
self.running = True
|
||||
|
||||
def on_close(self, event):
|
||||
self.running = False
|
||||
|
||||
def __call__(self, interval):
|
||||
try:
|
||||
plt.pause(interval)
|
||||
except Exception:
|
||||
pass
|
||||
return self.running
|
||||
|
||||
|
||||
def rect(x1, x2, y1, y2):
|
||||
return np.array([[x1,x2,x2,x1,x1],[y1,y1,y2,y2,y1]])
|
||||
|
||||
NAN = float('nan')
|
||||
|
||||
def rects(intervals, y12):
|
||||
result = [rect(*intervals[0], *y12)]
|
||||
@ -19,13 +68,19 @@ def rects(intervals, y12):
|
||||
result.append(rect(*x12, *y12))
|
||||
return np.concatenate(result, axis=1)
|
||||
|
||||
|
||||
class Plot:
|
||||
def __init__(self, maxy):
|
||||
def __init__(self, maxy, maxx=None):
|
||||
self.lines = {}
|
||||
self.yaxis = ((-2 * maxy, maxy), (-maxy, 2 * maxy))
|
||||
self.maxx = maxx
|
||||
self.first = True
|
||||
self.fig = None
|
||||
|
||||
|
||||
def pause(self, interval):
|
||||
"""will be overridden when figure is created"""
|
||||
return False
|
||||
|
||||
def set_line(self, iax, name, data, fmt, **kwds):
|
||||
"""
|
||||
plot or update a line
|
||||
@ -68,8 +123,9 @@ class Plot:
|
||||
if self.first:
|
||||
plt.ion()
|
||||
self.fig, axleft = plt.subplots(figsize=(15,7))
|
||||
self.pause = Pause(self.fig)
|
||||
plt.title("I/Q", fontsize=14)
|
||||
axleft.set_xlim(0, curves[0][-1])
|
||||
axleft.set_xlim(0, self.maxx or curves[0][-1])
|
||||
self.ax = [axleft, axleft.twinx()]
|
||||
self.ax[0].axhline(y=0, color='#cccccc') # show x-axis line
|
||||
self.ax[1].axhline(y=0, color='#cccccc')
|
||||
@ -95,7 +151,8 @@ class Plot:
|
||||
plt.tight_layout()
|
||||
finally:
|
||||
self.first = False
|
||||
|
||||
|
||||
plt.draw()
|
||||
# TODO: do not know why this is needed:
|
||||
self.fig.canvas.draw()
|
||||
self.fig.canvas.flush_events()
|
||||
|
File diff suppressed because it is too large
Load Diff
262
frappy_psi/logo.py
Normal file
262
frappy_psi/logo.py
Normal file
@ -0,0 +1,262 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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
|
||||
#
|
||||
#
|
||||
#
|
||||
# *****************************************************************************
|
||||
from ast import literal_eval
|
||||
import snap7
|
||||
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType,IDLE, BUSY, WARN, ERROR,Writable, Drivable, BoolType, IntRange, Communicator
|
||||
from frappy.errors import CommunicationFailedError
|
||||
from threading import RLock
|
||||
import sys
|
||||
import time
|
||||
|
||||
class IO(Communicator):
|
||||
|
||||
|
||||
tcap_client = Property('tcap_client', IntRange())
|
||||
tsap_server = Property('tcap_server', IntRange())
|
||||
ip_address = Property('numeric ip address', StringType())
|
||||
_plc = None
|
||||
_last_try = 0
|
||||
|
||||
def initModule(self):
|
||||
self._lock = RLock()
|
||||
super().initModule()
|
||||
def _init(self):
|
||||
if not self._plc:
|
||||
if time.time() < self._last_try + 10:
|
||||
raise CommunicationFailedError('logo PLC not reachable')
|
||||
self._plc = snap7.logo.Logo()
|
||||
prev_stderr = sys.stdout
|
||||
sys.stderr = open('/dev/null', 'w') # suppress output of snap7
|
||||
try:
|
||||
self._plc.connect(self.ip_address, self.tcap_client, self.tsap_server)
|
||||
if self._plc.get_connected():
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
sys.stderr = prev_stderr
|
||||
self._plc = None
|
||||
self._last_try = time.time()
|
||||
raise CommunicationFailedError('logo PLC not reachable')
|
||||
|
||||
|
||||
|
||||
def communicate(self, cmd):
|
||||
with self._lock:
|
||||
self._init()
|
||||
cmd = cmd.split(maxsplit=1)
|
||||
if len(cmd) == 2:
|
||||
self._plc.write(cmd[0], literal_eval(cmd[1]))
|
||||
try:
|
||||
return self._plc.read(cmd[0])
|
||||
except Exception as e:
|
||||
if self._plc:
|
||||
self.log.exception('error in plc read')
|
||||
self._plc = None
|
||||
raise
|
||||
|
||||
|
||||
|
||||
class Snap7Mixin(HasIO):
|
||||
ioclass = IO
|
||||
|
||||
def get_vm_value(self, vm_address):
|
||||
return self.io.communicate(vm_address)
|
||||
|
||||
|
||||
def set_vm_value(self, vm_address, value):
|
||||
return self.io.communicate(f'{vm_address} {value}')
|
||||
|
||||
class Pressure(Snap7Mixin, Readable):
|
||||
vm_address = Property('VM address', datatype= StringType())
|
||||
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(Snap7Mixin, 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(Snap7Mixin, 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.001)
|
||||
|
||||
def read_status(self):
|
||||
self.log.info('read_status')
|
||||
value = self.read_value()
|
||||
self.log.info('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(Snap7Mixin, Drivable):
|
||||
vm_address_output = Property('VM address output', datatype= StringType())
|
||||
|
||||
target = Parameter('Valve target', datatype = BoolType())
|
||||
value = Parameter('Value 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(Snap7Mixin, Readable):
|
||||
vm_address = Property('VM address', datatype= StringType())
|
||||
value = Parameter('resistance', datatype = FloatRange(unit = 'Ohm'))
|
||||
|
||||
|
||||
def read_value(self):
|
||||
return self.get_vm_value(self.vm_address)
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
|
||||
class HeaterParam(Snap7Mixin, Writable):
|
||||
vm_address = Property('VM address output', datatype= StringType())
|
||||
|
||||
target = Parameter('Heater target', datatype = IntRange())
|
||||
|
||||
value = Parameter('Heater Param', datatype = IntRange())
|
||||
|
||||
def read_value(self):
|
||||
return self.get_vm_value(self.vm_address)
|
||||
|
||||
def write_target(self, target):
|
||||
return self.set_vm_value(self.vm_address, target)
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
|
||||
|
||||
class controlHeater(Snap7Mixin, 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(Snap7Mixin, 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(Snap7Mixin, 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(Snap7Mixin, 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, ''
|
||||
|
||||
|
||||
|
||||
|
@ -166,6 +166,7 @@ class Switcher(LakeShoreIO, ChannelSwitcher):
|
||||
|
||||
def set_active_channel(self, chan):
|
||||
self.set_param('SCAN ', chan.channel, 0)
|
||||
self.value = chan.channel
|
||||
chan._last_range_change = time.monotonic()
|
||||
self.set_delays(chan)
|
||||
|
||||
@ -278,7 +279,12 @@ class ResChannel(LakeShoreIO, Channel):
|
||||
vexc = 0 if excoff or iscur else exc
|
||||
if (rng, iexc, vexc) != (self.range, self.iexc, self.vexc):
|
||||
self._last_range_change = time.monotonic()
|
||||
self.range, self.iexc, self.vexc = rng, iexc, vexc
|
||||
try:
|
||||
self.range, self.iexc, self.vexc = rng, iexc, vexc
|
||||
except Exception:
|
||||
# avoid raising errors on disabled channel
|
||||
if self.enabled:
|
||||
raise
|
||||
|
||||
@CommonWriteHandler(rdgrng_params)
|
||||
def write_rdgrng(self, change):
|
||||
|
38
frappy_psi/manual_valves.py
Normal file
38
frappy_psi/manual_valves.py
Normal file
@ -0,0 +1,38 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO, \
|
||||
Property, StringType, Writable, IntRange, IDLE, BUSY, ERROR
|
||||
from frappy.errors import CommunicationFailedError
|
||||
|
||||
class ManualValve(Writable):
|
||||
target = Parameter('Valve target', datatype = BoolType())
|
||||
value = Parameter('Valve state', datatype = BoolType())
|
||||
|
||||
def read_value(self):
|
||||
return self.value
|
||||
|
||||
def write_target(self, target):
|
||||
self.value = target
|
||||
return self.value
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
@ -375,6 +375,7 @@ class HeaterOutput(HasInput, Writable):
|
||||
|
||||
class HeaterUpdate(HeaterOutput):
|
||||
kind = 'HTR,TEMP'
|
||||
target = 0 # switch off loop on startup
|
||||
|
||||
def update_target(self, module, value):
|
||||
self.change(f'DEV::TEMP:LOOP:ENAB', False, off_on)
|
||||
|
@ -21,12 +21,12 @@
|
||||
|
||||
"""modules to access parameters"""
|
||||
|
||||
import re
|
||||
from frappy.core import Drivable, EnumType, IDLE, Attached, StringType, Property, \
|
||||
Parameter, BoolType, FloatRange, Readable, ERROR, nopoll
|
||||
from frappy.errors import ConfigError
|
||||
from frappy_psi.convergence import HasConvergence
|
||||
from frappy_psi.mixins import HasRamp
|
||||
from frappy.lib import merge_status
|
||||
|
||||
|
||||
class Par(Readable):
|
||||
@ -255,3 +255,41 @@ class SwitchDriv(HasConvergence, Drivable):
|
||||
self.log.info('target=%g (%s)', target, this.name)
|
||||
this.write_target(target1)
|
||||
return target
|
||||
|
||||
|
||||
INDEX = re.compile(r'(.*)\[(.*)\]')
|
||||
|
||||
|
||||
class Comp(Readable):
|
||||
value = Parameter(datatype=FloatRange(unit='$'))
|
||||
read = Attached(description='<module>.<parameter> for read')
|
||||
unit = Property('main unit', StringType())
|
||||
|
||||
_parname = None
|
||||
_index = None
|
||||
|
||||
def setProperty(self, key, value):
|
||||
if key == 'read':
|
||||
value, param = value.split('.')
|
||||
match = INDEX.match(param)
|
||||
if match:
|
||||
self._param, i = match.groups()
|
||||
self._index = int(i)
|
||||
else:
|
||||
self._param = param
|
||||
super().setProperty(key, value)
|
||||
|
||||
def checkProperties(self):
|
||||
self.applyMainUnit(self.unit)
|
||||
if self._param == self.name:
|
||||
raise ConfigError('illegal recursive read module')
|
||||
super().checkProperties()
|
||||
|
||||
def read_value(self):
|
||||
par = getattr(self.read, self._param)
|
||||
if self._index is None:
|
||||
return par
|
||||
return par[self._index]
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
|
189
frappy_psi/pfeiffer_new.py
Normal file
189
frappy_psi/pfeiffer_new.py
Normal file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Mon Apr 29 09:24:07 2024
|
||||
@author: andreaplank
|
||||
"""
|
||||
from frappy.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO, \
|
||||
Property, StringType, Drivable, IntRange, IDLE, BUSY, ERROR, nopoll
|
||||
from frappy.errors import CommunicationFailedError
|
||||
|
||||
|
||||
class PfeifferProtocol(StringIO):
|
||||
end_of_line = '\r'
|
||||
|
||||
class PfeifferMixin(HasIO):
|
||||
ioClass = PfeifferProtocol
|
||||
address= Property('Addresse', datatype= IntRange())
|
||||
|
||||
def calculate_crc(self, data):
|
||||
crc = sum(ord(chr) for chr in data) % 256
|
||||
return f'{crc:03d}'
|
||||
|
||||
def check_crc(self, data):
|
||||
if data [-3:] != self.calculate_crc(data[:-3]):
|
||||
raise CommunicationFailedError('Bad crc')
|
||||
|
||||
def data_request_u_expo_new(self, parameter_nr):
|
||||
cmd = f'{self.address:03d}00{parameter_nr:03d}02=?'
|
||||
cmd += self.calculate_crc(cmd)
|
||||
|
||||
reply = self.communicate(cmd)
|
||||
self.check_crc(reply)
|
||||
|
||||
|
||||
assert int(reply[5:8]) == parameter_nr
|
||||
|
||||
assert int(reply[0:3]) == self.address
|
||||
|
||||
try:
|
||||
exponent = int(reply[14:16])-23
|
||||
except ValueError:
|
||||
raise CommunicationFailedError(f'got {reply[10:16]}')
|
||||
|
||||
|
||||
return float(f'{reply[10:14]}e{exponent}')
|
||||
|
||||
def data_request_old_boolean(self, parameter_nr):
|
||||
|
||||
|
||||
cmd = f'{self.address:03d}00{parameter_nr:03d}02=?'
|
||||
cmd += self.calculate_crc(cmd)
|
||||
|
||||
reply = self.communicate(cmd)
|
||||
self.check_crc(reply)
|
||||
assert int(reply[5:8]) == parameter_nr, f"Parameter number mismatch: expected {parameter_nr}, got {int(reply[5:8])}"
|
||||
|
||||
assert int(reply[0:3]) == self.address, f"Address mismatch: expected {self.address}, got {int(reply[0:3])}"
|
||||
|
||||
|
||||
if reply[12] == "1":
|
||||
value = True
|
||||
elif reply[12] == "0":
|
||||
value = False
|
||||
else:
|
||||
raise CommunicationFailedError(f'got {reply[10:16]}')
|
||||
|
||||
return value
|
||||
|
||||
def data_request_u_real(self, parameter_nr):
|
||||
cmd = f'{self.address:03d}00{parameter_nr:03d}02=?'
|
||||
cmd += self.calculate_crc(cmd)
|
||||
|
||||
reply = self.communicate(cmd)
|
||||
self.check_crc(reply)
|
||||
|
||||
assert int(reply[5:8]) == parameter_nr
|
||||
|
||||
assert int(reply[0:3]) == self.address
|
||||
|
||||
try:
|
||||
value = float(reply[10:16])/100
|
||||
except ValueError:
|
||||
raise CommunicationFailedError(f'got {reply[10:16]}')
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def data_request_u_int(self, parameter_nr):
|
||||
cmd = f'{self.address:03d}00{parameter_nr:03d}02=?'
|
||||
cmd += self.calculate_crc(cmd)
|
||||
|
||||
reply = self.communicate(cmd)
|
||||
self.check_crc(reply)
|
||||
|
||||
if reply[8] == "0":
|
||||
reply_length = (int)(reply[9])
|
||||
else:
|
||||
reply_length = (int)(reply[8:10])
|
||||
|
||||
try:
|
||||
if reply[10 : 10 + reply_length] == "000000":
|
||||
value = 0
|
||||
else:
|
||||
value = float(reply[10 : 10 + reply_length].lstrip("0"))
|
||||
except ValueError:
|
||||
raise CommunicationFailedError(f'got {reply[10:16]}')
|
||||
|
||||
return value
|
||||
|
||||
def data_request_string(self, parameter_nr):
|
||||
cmd = f'{self.address:03d}00{parameter_nr:03d}02=?'
|
||||
cmd += self.calculate_crc(cmd)
|
||||
|
||||
reply = self.communicate(cmd)
|
||||
self.check_crc(reply)
|
||||
|
||||
assert int(reply[5:8]) == parameter_nr
|
||||
|
||||
assert int(reply[0:3]) == self.address
|
||||
|
||||
return str(reply[10:16])
|
||||
|
||||
|
||||
def control_old_boolean(self, parameter_nr, target):
|
||||
if target:
|
||||
val = 1
|
||||
else:
|
||||
val = 0
|
||||
|
||||
cmd = f'{self.address:03d}10{parameter_nr:03d}06{str(val)*6}'
|
||||
cmd += self.calculate_crc(cmd)
|
||||
|
||||
reply = self.communicate(cmd)
|
||||
self.check_crc(reply)
|
||||
|
||||
assert cmd == reply, f'got {reply} instead of {cmd} '
|
||||
|
||||
try:
|
||||
if reply[11] == "1":
|
||||
value = 1
|
||||
else:
|
||||
value = 0
|
||||
except ValueError:
|
||||
|
||||
raise CommunicationFailedError(f'got {reply[10:16]}')
|
||||
|
||||
return value
|
||||
|
||||
class RPT200(PfeifferMixin, Readable):
|
||||
value = Parameter('Pressure', FloatRange(unit='hPa'))
|
||||
|
||||
def read_value(self):
|
||||
return self.data_request_u_expo_new(740)
|
||||
def read_status(self):
|
||||
errtxt = self.data_request_string(303)
|
||||
if errtxt == "000000":
|
||||
return IDLE, ''
|
||||
else:
|
||||
return ERROR, errtxt
|
||||
|
||||
class TCP400(PfeifferMixin, Drivable, Readable):
|
||||
speed= Parameter('Rotational speed', FloatRange(unit = 'Hz'), readonly = False)
|
||||
target= Parameter('Pumping station', BoolType())
|
||||
current= Parameter('Current consumption', FloatRange(unit = '%'))
|
||||
value = Parameter('Turbopump state', BoolType())
|
||||
temp = Parameter('temp', FloatRange(unit = 'C'))
|
||||
def read_temp (self):
|
||||
return self.data_request_u_int(326)
|
||||
|
||||
def read_speed(self):
|
||||
return self.data_request_u_int(309)
|
||||
|
||||
def read_value(self):
|
||||
return self.data_request_old_boolean(10)
|
||||
|
||||
def read_current(self):
|
||||
return self.data_request_u_real(310)
|
||||
|
||||
def write_target(self, target):
|
||||
return self.control_old_boolean(10, target)
|
||||
|
||||
def read_target(self):
|
||||
return self.data_request_old_boolean(10)
|
||||
|
||||
def read_status(self):
|
||||
if not self.data_request_old_boolean(306):
|
||||
return BUSY, 'ramping up'
|
||||
else:
|
||||
return IDLE,'at targetspeed'
|
@ -166,7 +166,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
def write_target(self, value):
|
||||
self.read_alive_time()
|
||||
if self._blocking_error:
|
||||
self.status = ERROR, '<motor>.clear_errors() needed after ' + self._blocking_error
|
||||
self.status = ERROR, 'clear_errors needed after ' + self._blocking_error
|
||||
raise HardwareError(self.status[1])
|
||||
self.saveParameters()
|
||||
self.start_machine(self.starting, target=value)
|
||||
|
@ -48,8 +48,8 @@ example cfg:
|
||||
Mod('T_softloop',
|
||||
'frappy_psi.picontrol.PI',
|
||||
'softloop controlled Temperature mixing chamber',
|
||||
input = 'ts',
|
||||
output = 'htr_mix',
|
||||
input_module = 'ts',
|
||||
output_module = 'htr_mix',
|
||||
control_active = 1,
|
||||
output_max = 80000,
|
||||
p = 2E6,
|
||||
@ -60,10 +60,10 @@ example cfg:
|
||||
|
||||
import time
|
||||
import math
|
||||
from frappy.core import Readable, Writable, Parameter, Attached, IDLE
|
||||
from frappy.core import Readable, Writable, Parameter, Attached, IDLE, Property
|
||||
from frappy.lib import clamp
|
||||
from frappy.datatypes import LimitsType, EnumType, BoolType, FloatRange
|
||||
from frappy.mixins import HasOutputModule
|
||||
from frappy.newmixins import HasOutputModule
|
||||
from frappy_psi.convergence import HasConvergence
|
||||
|
||||
|
||||
@ -71,32 +71,27 @@ class PImixin(HasOutputModule, Writable):
|
||||
p = Parameter('proportional term', FloatRange(0), readonly=False)
|
||||
i = Parameter('integral term', FloatRange(0), readonly=False)
|
||||
# output_module is inherited
|
||||
output_range = Parameter('min output',
|
||||
LimitsType(FloatRange()), default=(0, 0), readonly=False)
|
||||
output_range = Property('legacy output range', LimitsType(FloatRange()), default=(0,0))
|
||||
output_min = Parameter('min output', FloatRange(), default=0, readonly=False)
|
||||
output_max = Parameter('max output', FloatRange(), default=0, readonly=False)
|
||||
output_func = Parameter('output function',
|
||||
EnumType(lin=0, square=1), readonly=False, default=0)
|
||||
value = Parameter(unit='K')
|
||||
_lastdiff = None
|
||||
_lasttime = 0
|
||||
_clamp_limits = None
|
||||
_get_range = None # a function get output range from output_module
|
||||
_overflow = 0
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
if self.output_range != (0, 0): # legacy !
|
||||
self.output_min, self.output_max = self.output_range
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
if self._clamp_limits is None:
|
||||
out = self.output_module
|
||||
if hasattr(out, 'max_target'):
|
||||
if hasattr(self, 'min_target'):
|
||||
self._clamp_limits = lambda v, o=out: clamp(v, o.read_min_target(), o.read_max_target())
|
||||
else:
|
||||
self._clamp_limits = lambda v, o=out: clamp(v, 0, o.read_max_target())
|
||||
elif hasattr(out, 'limit'): # mercury.HeaterOutput
|
||||
self._clamp_limits = lambda v, o=out: clamp(v, 0, o.read_limit())
|
||||
else:
|
||||
self._clamp_limits = lambda v: v
|
||||
if self.output_range == (0.0, 0.0):
|
||||
self.output_range = (0, self._clamp_limits(float('inf')))
|
||||
if not self.control_active:
|
||||
return
|
||||
out = self.output_module
|
||||
self.status = IDLE, 'controlling'
|
||||
now = time.time()
|
||||
deltat = clamp(0, now-self._lasttime, 10)
|
||||
@ -106,17 +101,51 @@ class PImixin(HasOutputModule, Writable):
|
||||
self._lastdiff = diff
|
||||
deltadiff = diff - self._lastdiff
|
||||
self._lastdiff = diff
|
||||
output, omin, omax = self._cvt2int(out.target)
|
||||
output += self._overflow + self.p * deltadiff + self.i * deltat * diff
|
||||
if output < omin:
|
||||
self._overflow = max(omin - omax, output - omin)
|
||||
output = omin
|
||||
elif output > omax:
|
||||
self._overflow = min(omax - omin, output - omax)
|
||||
output = omax
|
||||
else:
|
||||
self._overflow = 0
|
||||
out.update_target(self.name, self._cvt2ext(output))
|
||||
|
||||
def cvt2int_square(self, output):
|
||||
return (math.sqrt(max(0, clamp(x, *self._get_range()))) for x in (output, self.output_min, self.output_max))
|
||||
|
||||
def cvt2ext_square(self, output):
|
||||
return output ** 2
|
||||
|
||||
def cvt2int_lin(self, output):
|
||||
return (clamp(x, *self._get_range()) for x in (output, self.output_min, self.output_max))
|
||||
|
||||
def cvt2ext_lin(self, output):
|
||||
return output
|
||||
|
||||
def write_output_func(self, value):
|
||||
out = self.output_module
|
||||
output = out.target
|
||||
if self.output_func == 'square':
|
||||
output = math.sqrt(max(0, output))
|
||||
output += self.p * deltadiff + self.i * deltat * diff
|
||||
if self.output_func == 'square':
|
||||
output = output ** 2
|
||||
output = self._clamp_limits(output)
|
||||
out.update_target(self.name, clamp(output, *self.output_range))
|
||||
if hasattr(out, 'max_target'):
|
||||
if hasattr(self, 'min_target'):
|
||||
self._get_range = lambda o=out: (o.read_min_target(), o.read_max_target())
|
||||
else:
|
||||
self._get_range = lambda o=out: (0, o.read_max_target())
|
||||
elif hasattr(out, 'limit'): # mercury.HeaterOutput
|
||||
self._get_range = lambda o=out: (0, o.read_limit())
|
||||
else:
|
||||
if self.output_min == self.output_max == 0:
|
||||
self.output_max = 1
|
||||
self._get_range = lambda o=self: (o.output_min, o.output_max)
|
||||
if self.output_min == self.output_max == 0:
|
||||
self.output_min, self.output_max = self._get_range()
|
||||
self.output_func = value
|
||||
self._cvt2int = getattr(self, f'cvt2int_{self.output_func.name}')
|
||||
self._cvt2ext = getattr(self, f'cvt2ext_{self.output_func.name}')
|
||||
|
||||
def write_control_active(self, value):
|
||||
super().write_control_active(value)
|
||||
if not value:
|
||||
self.output_module.write_target(0)
|
||||
|
||||
@ -125,61 +154,6 @@ class PImixin(HasOutputModule, Writable):
|
||||
self.activate_control()
|
||||
|
||||
|
||||
# quick fix by Marek:
|
||||
class PIobsolete(Writable):
|
||||
"""temporary, but working version from Marek"""
|
||||
input = Attached(Readable, 'the input module')
|
||||
output = Attached(Writable, 'the output module')
|
||||
output_max = Parameter('max output value', FloatRange(0), readonly=False)
|
||||
p = Parameter('proportional term', FloatRange(0), readonly=False)
|
||||
i = Parameter('integral term', FloatRange(0), readonly=False)
|
||||
control_active = Parameter('control flag', BoolType(), readonly=False, default=False)
|
||||
value = Parameter(unit='K')
|
||||
tlim = Parameter('max Temperature', FloatRange(0), readonly=False)
|
||||
_lastdiff = None
|
||||
_lasttime = 0
|
||||
_lastvalue = 0
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
if not self.control_active:
|
||||
return
|
||||
self.value = self.input.value
|
||||
self.status = IDLE, 'controlling'
|
||||
now = time.time()
|
||||
deltat = min(10.0, now-self._lasttime)
|
||||
self._lasttime = now
|
||||
if self.value != self._lastvalue:
|
||||
diff = self.target - self.value # calculate the difference to target
|
||||
self._lastvalue = self.value
|
||||
# else ? (diff is undefined!)
|
||||
if self.value > self.tlim:
|
||||
self.write_control_active(False)
|
||||
return
|
||||
if self._lastdiff is None:
|
||||
self._lastdiff = diff
|
||||
deltadiff = diff - self._lastdiff # calculate the change in deltaT
|
||||
self._lastdiff = diff
|
||||
output = self.output.target
|
||||
output += self.p * deltadiff + self.i * deltat * diff
|
||||
if output > self.output_max:
|
||||
output = self.output_max
|
||||
elif output < 0:
|
||||
output = 0
|
||||
self.output.write_target(output)
|
||||
|
||||
def write_control_active(self, value):
|
||||
if not value:
|
||||
self.output.write_target(0)
|
||||
|
||||
def write_target(self, value):
|
||||
self.control_active = True
|
||||
|
||||
|
||||
# proposal for replacing above PI class, inheriting from PImixin
|
||||
# additional features:
|
||||
# - is a Drivable, using the convergence criteria from HasConvergence
|
||||
# - tries to determine the output limits automatically
|
||||
# unchecked!
|
||||
|
||||
class PI(HasConvergence, PImixin):
|
||||
@ -190,3 +164,18 @@ class PI(HasConvergence, PImixin):
|
||||
|
||||
def read_status(self):
|
||||
return self.input_module.status
|
||||
|
||||
|
||||
class PI2(PI):
|
||||
maxovershoot = Parameter('max. overshoot', FloatRange(0, 100, unit='%'), readonly=False, default=20)
|
||||
|
||||
def doPoll(self):
|
||||
self.output_max = self.target * (1 + 0.01 * self.maxovershoot)
|
||||
self.output_min = self.target * (1 - 0.01 * self.maxovershoot)
|
||||
super().doPoll()
|
||||
|
||||
def write_target(self, target):
|
||||
if not self.control_active:
|
||||
self.output.write_target(target)
|
||||
super().write_target(target)
|
||||
|
||||
|
96
frappy_psi/sensirion.py
Normal file
96
frappy_psi/sensirion.py
Normal file
@ -0,0 +1,96 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""sensirion flow sensor,
|
||||
|
||||
connected via an Arduio Nano on a serial connection
|
||||
shared with the trinamic hepump valve motor
|
||||
"""
|
||||
|
||||
import math
|
||||
from frappy.core import Parameter, Readable, IntRange, FloatRange, BoolType, BytesIO, HasIO
|
||||
from frappy.errors import ProgrammingError
|
||||
|
||||
|
||||
class FlowSensor(HasIO, Readable):
|
||||
value = Parameter(unit='ln/min')
|
||||
stddev = Parameter('std dev.', FloatRange(unit='ln/min'))
|
||||
nsamples = Parameter('number of samples for averaging', IntRange(1,1024), default=160)
|
||||
offset = Parameter('offset correction', FloatRange(unit='ln/min'), readonly=False, default=0)
|
||||
scale = Parameter('scale factor', FloatRange(), readonly=False, default=2.3)
|
||||
saved = Parameter('is the current value saved?', BoolType(), readonly=False)
|
||||
pollinterval = Parameter(default=0.2)
|
||||
|
||||
ioClass = BytesIO
|
||||
_saved = None
|
||||
|
||||
def command(self, cmd, nvalues=1):
|
||||
if len(cmd) == 1: # its a query
|
||||
command = f'{cmd}\n'
|
||||
else:
|
||||
if len(cmd) > 7:
|
||||
raise ProgrammingError('number does not fit into 6 characters')
|
||||
command = f'{cmd[0]}{cmd[1:].ljust(6)}\n'
|
||||
reply = self.io.communicate(command.encode('ascii'), max(1, nvalues * 9))
|
||||
if nvalues == 1:
|
||||
return float(reply)
|
||||
if nvalues:
|
||||
return tuple(float(s) for s in reply.split())
|
||||
return None
|
||||
|
||||
def doPoll(self):
|
||||
flow, stddev = self.command('?', nvalues=2)
|
||||
stddev = stddev / math.sqrt(self.nsamples)
|
||||
if (flow, stddev) != (self.value, self.stddev):
|
||||
self.value, self.stddev = flow, stddev
|
||||
# TODO: treat status (e.g. when reading 0 always)
|
||||
|
||||
def read_value(self):
|
||||
self.doPoll()
|
||||
return self.value
|
||||
|
||||
def read_nsamples(self):
|
||||
return self.command('n')
|
||||
|
||||
def write_nsamples(self, nsamples):
|
||||
return self.command(f'n{nsamples}')
|
||||
|
||||
def read_offset(self):
|
||||
return self.command('o')
|
||||
|
||||
def write_offset(self, offset):
|
||||
return self.command(f'o{offset:.2f}')
|
||||
|
||||
def read_scale(self):
|
||||
return self.command('g')
|
||||
|
||||
def write_scale(self, scale):
|
||||
return self.command(f'g{scale:.4f}')
|
||||
|
||||
def read_saved(self):
|
||||
if self._saved is None:
|
||||
self._saved = self.read_scale(), self.read_offset()
|
||||
return True
|
||||
return self._saved == (self.scale, self.offset)
|
||||
|
||||
def write_saved(self, target):
|
||||
if target:
|
||||
self.command('s', nvalues=0)
|
@ -17,31 +17,38 @@
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# Leon Zimmermann <leon.zimmermann@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Powersupply TDK-Lambda GEN8-400-1P230"""
|
||||
"""Powersupply TDK-Lambda GEN8-400-3P400"""
|
||||
|
||||
from frappy.core import StringIO, Readable, Parameter, Writable, HasIO
|
||||
from frappy.datatypes import BoolType, EnumType, FloatRange
|
||||
|
||||
from frappy.core import StringIO, Readable, Parameter, FloatRange, Writable, HasIO, BoolType
|
||||
|
||||
class IO(StringIO):
|
||||
end_of_line = ('OK\r', '\r')
|
||||
end_of_line = '\r'
|
||||
default_settings = {'baudrate': 9600}
|
||||
identification = [('ADR 0', 'OK'), ('IDN?', r'LAMBDA,GEN8-400')]
|
||||
|
||||
|
||||
|
||||
class Power(HasIO, Readable):
|
||||
value = Parameter(datatype=FloatRange(0,3300,unit='W'))
|
||||
|
||||
voltage = Parameter('voltage', FloatRange(0,8, unit='V'))
|
||||
current = Parameter('current', FloatRange(0,400, unit='A'))
|
||||
|
||||
def read_value(self):
|
||||
reply_volt = self.communicate('MV?')
|
||||
reply_current = self.communicate('MC?')
|
||||
volt = float(reply_volt)
|
||||
current = float(reply_current)
|
||||
return volt*current
|
||||
self.voltage = float(self.communicate('MV?'))
|
||||
self.current = float(self.communicate('MC?'))
|
||||
return self.voltage * self.current
|
||||
|
||||
|
||||
class Output(HasIO, Writable):
|
||||
value = Parameter(datatype=FloatRange(0,100,unit='%'))
|
||||
value = Parameter(datatype=FloatRange(0,100,unit='%'), default=0)
|
||||
target = Parameter(datatype=FloatRange(0,100,unit='%'))
|
||||
maxvolt = Parameter('voltage at 100%',datatype=FloatRange(0,8,unit='V'),readonly=False)
|
||||
maxcurrent = Parameter('current at 100%',datatype=FloatRange(0,400,unit='A'),readonly=False)
|
||||
mode = Parameter('regulation mode', EnumType(voltage=1, current=2, both=3),
|
||||
default='voltage', readonly=False)
|
||||
maxvolt = Parameter('voltage at 100%',
|
||||
datatype=FloatRange(0,8,unit='V'), readonly=False)
|
||||
maxcurrent = Parameter('current at 100%',
|
||||
datatype=FloatRange(0,400,unit='A'), readonly=False)
|
||||
output_enable = Parameter('control on/off', BoolType(), readonly=False)
|
||||
|
||||
def initModule(self):
|
||||
@ -49,11 +56,22 @@ class Output(HasIO, Writable):
|
||||
self.write_output_enable(False)
|
||||
|
||||
def write_target(self, target):
|
||||
self.write_output_enable(target != 0)
|
||||
self.communicate(f'PV {target*self.maxvolt:.5f}')
|
||||
self.communicate(f'PC {target*self.maxcurrent:.5f}')
|
||||
# take care of proper order
|
||||
if target == 0:
|
||||
self.write_output_enable(False)
|
||||
prev_curr = float(self.communicate(f'PC?'))
|
||||
volt = self.maxvolt if self.mode == 'current' else self.maxvolt * 0.01 * target
|
||||
curr = self.maxcurrent if self.mode == 'voltage' else self.maxcurrent * 0.01 * target
|
||||
if curr < prev_curr:
|
||||
self.communicate(f'PC {curr:.6g}')
|
||||
self.communicate(f'PV {volt:.6g}')
|
||||
else:
|
||||
self.communicate(f'PV {volt:.6g}')
|
||||
self.communicate(f'PC {curr:.6g}')
|
||||
if target:
|
||||
self.write_output_enable(True)
|
||||
self.value = target
|
||||
|
||||
|
||||
def write_output_enable(self, value):
|
||||
self.communicate(f'OUT {int(value)}')
|
||||
|
||||
|
@ -26,7 +26,8 @@ import struct
|
||||
|
||||
from frappy.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
||||
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done, \
|
||||
IDLE, BUSY, ERROR, Limit
|
||||
IDLE, BUSY, ERROR, Limit, nopoll, ArrayOf
|
||||
from frappy.properties import HasProperties
|
||||
from frappy.io import BytesIO
|
||||
from frappy.errors import CommunicationFailedError, HardwareError, RangeError, IsBusyError
|
||||
from frappy.rwhandler import ReadHandler, WriteHandler
|
||||
@ -119,9 +120,6 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
move_status = Parameter('', EnumType(ok=0, stalled=1, encoder_deviation=2, stalled_and_encoder_deviation=3),
|
||||
group='hwstatus')
|
||||
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
|
||||
home = Parameter('state of home switch (input 3)', BoolType(), group='more')
|
||||
has_home = Parameter('enable home and activate pullup resistor', BoolType(),
|
||||
default=True, group='more')
|
||||
auto_reset = Parameter('automatic reset after failure', BoolType(), group='more', readonly=False, default=False)
|
||||
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||
value=0.1, group='motorparam')
|
||||
@ -132,6 +130,20 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
readonly=False, default=0, visibility=3, group='more')
|
||||
max_retry = Parameter('maximum number of retries', IntRange(0, 99), readonly=False, default=0)
|
||||
stall_thr = Parameter('stallGuard threshold', IntRange(-64, 63), readonly=False, value=0)
|
||||
input_bits = Parameter('input bits', IntRange(0, 255), group='more', export=False)
|
||||
input1 = Parameter('input 1', BoolType(), export=False, group='more')
|
||||
input2 = Parameter('input 2', BoolType(), export=False, group='more')
|
||||
input3 = Parameter('input 3', BoolType(), export=False, group='more')
|
||||
output0 = Parameter('output 0', BoolType(), readonly=False, export=False, group='more', default=0)
|
||||
output1 = Parameter('output 1', BoolType(), readonly=False, export=False, group='more', default=0)
|
||||
home = Parameter('state of home switch (input 3)', BoolType(), group='more', export=False)
|
||||
has_home = Property('enable home and activate pullup resistor', BoolType(), export=False,
|
||||
default=True)
|
||||
has_inputs = Property('inputs are polled', BoolType(), export=False,
|
||||
default=False)
|
||||
with_pullup = Property('activate pullup', BoolType(), export=False,
|
||||
default=True)
|
||||
|
||||
pollinterval = Parameter(group='more')
|
||||
target_min = Limit()
|
||||
target_max = Limit()
|
||||
@ -145,6 +157,18 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
_loading = False # True when loading parameters
|
||||
_drv_try = 0
|
||||
|
||||
def checkProperties(self):
|
||||
super().checkProperties()
|
||||
if self.has_home:
|
||||
self.parameters['home'].export = '_home'
|
||||
self.setProperty('has_inputs', True)
|
||||
if not self.has_inputs:
|
||||
self.setProperty('with_pullup', False)
|
||||
|
||||
def writeInitParams(self):
|
||||
super().writeInitParams()
|
||||
self.comm(SET_IO, 0, self.with_pullup)
|
||||
|
||||
def comm(self, cmd, adr, value=0, bank=0):
|
||||
"""set or get a parameter
|
||||
|
||||
@ -402,13 +426,6 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
def read_steppos(self):
|
||||
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
|
||||
|
||||
def read_home(self):
|
||||
return not self.comm(GET_IO, 255) & 8
|
||||
|
||||
def write_has_home(self, value):
|
||||
"""activate pullup resistor"""
|
||||
return bool(self.comm(SET_IO, 0, value))
|
||||
|
||||
@Command(FloatRange())
|
||||
def set_zero(self, value):
|
||||
"""adapt zero to make current position equal to given value"""
|
||||
@ -459,3 +476,47 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
def set_axis_par(self, adr, value):
|
||||
"""set arbitrary motor parameter"""
|
||||
return self.comm(SET_AXIS_PAR, adr, value)
|
||||
|
||||
def read_input_bits(self):
|
||||
if not self.has_inputs:
|
||||
return 0
|
||||
bits = self.comm(GET_IO, 255)
|
||||
self.input1 = bool(bits & 2)
|
||||
self.input2 = bool(bits & 4)
|
||||
self.input3 = bool(bits & 8)
|
||||
self.home = not self.input3
|
||||
return bits
|
||||
|
||||
@nopoll
|
||||
def read_home(self):
|
||||
self.read_input_bits()
|
||||
return self.home
|
||||
|
||||
@nopoll
|
||||
def read_input1(self):
|
||||
self.read_input_bits()
|
||||
return self.input1
|
||||
|
||||
@nopoll
|
||||
def read_input2(self):
|
||||
self.read_input_bits()
|
||||
return self.input2
|
||||
|
||||
@nopoll
|
||||
def read_input3(self):
|
||||
self.read_input_bits()
|
||||
return self.input3
|
||||
|
||||
def write_output0(self, value):
|
||||
return self.comm(SET_IO, 0, value, bank=2)
|
||||
|
||||
@nopoll
|
||||
def read_output0(self):
|
||||
return self.comm(GET_IO, 0, bank=2)
|
||||
|
||||
def write_output1(self, value):
|
||||
return self.comm(SET_IO, 1, value, bank=2)
|
||||
|
||||
@nopoll
|
||||
def read_output1(self):
|
||||
return self.comm(GET_IO, 1, bank=2)
|
||||
|
@ -25,11 +25,13 @@ import time
|
||||
import numpy as np
|
||||
|
||||
from frappy_psi.adq_mr import Adq, PEdata, RUSdata
|
||||
from frappy.core import Attached, BoolType, Done, FloatRange, HasIO, \
|
||||
IntRange, Module, Parameter, Readable, Writable, Drivable, StringIO, StringType, \
|
||||
IDLE, BUSY, DISABLED, ERROR, TupleOf, ArrayOf, Command
|
||||
from frappy.core import Attached, BoolType, Done, FloatRange, HasIO, StatusType, \
|
||||
IntRange, Module, Parameter, Readable, Writable, StatusType, StringIO, StringType, \
|
||||
IDLE, BUSY, DISABLED, WARN, ERROR, TupleOf, ArrayOf, Command, Attached, EnumType ,\
|
||||
Drivable
|
||||
from frappy.properties import Property
|
||||
#from frappy.modules import Collector
|
||||
from frappy.lib import clamp
|
||||
# from frappy.modules import Collector
|
||||
|
||||
Collector = Readable
|
||||
|
||||
@ -46,12 +48,13 @@ def fname_from_time(t, extension):
|
||||
|
||||
class Roi(Readable):
|
||||
main = Attached()
|
||||
a = Attached(mandatory=False) # amplitude Readable
|
||||
p = Attached(mandatory=False) # phase Readable
|
||||
i = Attached(mandatory=False) # i Readable
|
||||
q = Attached(mandatory=False) # amplitude Readable
|
||||
|
||||
value = Parameter('amplitude', FloatRange(), default=0)
|
||||
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||
i = Parameter('in phase', FloatRange(), default=0)
|
||||
q = Parameter('out of phase', FloatRange(), default=0)
|
||||
time = Parameter('start time', FloatRange(unit='nsec'), readonly=False)
|
||||
value = Parameter('i, q', TupleOf(FloatRange(), FloatRange()), default=(0, 0))
|
||||
time = Parameter('mid time', FloatRange(unit='nsec'), readonly=False)
|
||||
size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
|
||||
enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
|
||||
pollinterval = Parameter(export=False)
|
||||
@ -61,25 +64,65 @@ class Roi(Readable):
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.main.register_roi(self)
|
||||
self.calc_interval()
|
||||
|
||||
def calc_interval(self):
|
||||
self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size)
|
||||
@property
|
||||
def interval(self):
|
||||
return self.time - 0.5 * self.size, self.time + 0.5 * self.size
|
||||
|
||||
def read_status(self):
|
||||
return (IDLE, '') if self.enable else (DISABLED, 'disabled')
|
||||
|
||||
def write_time(self, value):
|
||||
self.time = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
def write_size(self, value):
|
||||
self.size = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
|
||||
class ControlRoi(Roi, Readable):
|
||||
freq = Attached()
|
||||
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000)
|
||||
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000)
|
||||
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6)
|
||||
control_active = Parameter('are we controlling?', BoolType(), readonly=False, default=0)
|
||||
|
||||
_freq_target = None
|
||||
_skipctrl = 2
|
||||
_old = None
|
||||
|
||||
def doPoll(self):
|
||||
inphase = self.value[0]
|
||||
freq = self.freq.target
|
||||
newfreq = None
|
||||
if freq != self._freq_target:
|
||||
self._freq_target = freq
|
||||
# do no control 2 times after changing frequency
|
||||
self._skipctrl = 2
|
||||
if self._old:
|
||||
fdif = freq - self._old[0]
|
||||
if self.control_active:
|
||||
newfreq = freq + inphase * self.slope
|
||||
self.log.info('fdif %r minstep %r', fdif, self.minstep)
|
||||
if abs(fdif) > self.minstep * 0.99:
|
||||
idif = inphase - self._old[1]
|
||||
if idif:
|
||||
self.slope = - fdif / idif
|
||||
self._old = (freq, inphase)
|
||||
else:
|
||||
self._old = (freq, inphase)
|
||||
if self.control_active:
|
||||
newfreq = freq + self.minstep
|
||||
if self._skipctrl > 0: # do no control for some time after changing frequency
|
||||
self._skipctrl -= 1
|
||||
elif newfreq is not None:
|
||||
self._freq_target = self.freq.write_target(clamp(freq - self.maxstep, newfreq, freq + self.maxstep))
|
||||
|
||||
|
||||
class Pars(Module):
|
||||
description = 'relevant parameters from SEA'
|
||||
|
||||
@ -90,36 +133,75 @@ class Pars(Module):
|
||||
|
||||
|
||||
class FreqStringIO(StringIO):
|
||||
end_of_line = '\r'
|
||||
end_of_line = '\r\n'
|
||||
|
||||
|
||||
class Frequency(HasIO, Writable):
|
||||
class Frequency(HasIO, Drivable):
|
||||
value = Parameter('frequency', unit='Hz')
|
||||
amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False)
|
||||
amp = Parameter('amplitude (VPP)', FloatRange(unit='V'), readonly=False)
|
||||
output = Parameter('output: L or R', EnumType(L=1, R=0), readonly=False, default='L')
|
||||
|
||||
last_change = 0
|
||||
ioClass = FreqStringIO
|
||||
dif = None
|
||||
_started = 0
|
||||
_within_write_target = False
|
||||
_nopoll_until = 0
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
if self.isBusy() and time.time() > self._started + 5:
|
||||
self.status = WARN, 'acquisition timeout'
|
||||
|
||||
def register_dif(self, dif):
|
||||
self.dif = dif
|
||||
|
||||
def read_value(self):
|
||||
if time.time() > self._nopoll_until or self.value == 0:
|
||||
self.value = float(self.communicate('FREQ?'))
|
||||
if self.dif:
|
||||
self.dif.read_value()
|
||||
return self.value
|
||||
|
||||
def set_busy(self):
|
||||
"""called by an acquisition module
|
||||
|
||||
from a callback on value within write_target
|
||||
"""
|
||||
if self._within_write_target:
|
||||
self._started = time.time()
|
||||
self.log.info('set busy')
|
||||
self.status = BUSY, 'waiting for acquisition'
|
||||
|
||||
def set_idle(self):
|
||||
if self.isBusy():
|
||||
self.status = IDLE, ''
|
||||
|
||||
def write_target(self, value):
|
||||
self.communicate('FREQ %.15g;FREQ?' % value)
|
||||
self.last_change = time.time()
|
||||
if self.dif:
|
||||
self.dif.read_value()
|
||||
self._nopoll_until = time.time() + 10
|
||||
try:
|
||||
self._within_write_target = True
|
||||
# may trigger busy=True from an acquisition module
|
||||
self.value = float(self.communicate('FREQ %.15g;FREQ?' % value))
|
||||
self.last_change = time.time()
|
||||
if self.dif:
|
||||
self.dif.read_value()
|
||||
return self.value
|
||||
finally:
|
||||
self._within_write_target = False
|
||||
|
||||
def write_amp(self, amp):
|
||||
reply = self.communicate('AMPR %g;AMPR?' % amp)
|
||||
return float(reply)
|
||||
self._nopoll_until = time.time() + 10
|
||||
self.amp = float(self.communicate(f'AMP{self.output.name} {amp} VPP;AMP{self.output.name}? VPP'))
|
||||
return self.amp
|
||||
|
||||
def read_amp(self):
|
||||
reply = self.communicate('AMPR?')
|
||||
return float(reply)
|
||||
if time.time() > self._nopoll_until or self.amp == 0:
|
||||
return float(self.communicate(f'AMP{self.output.name}? VPP'))
|
||||
return self.amp
|
||||
|
||||
|
||||
class FrequencyDif(Readable):
|
||||
class FrequencyDif(Drivable):
|
||||
freq = Attached(Frequency)
|
||||
base = Parameter('base frequency', FloatRange(unit='Hz'), default=0)
|
||||
value = Parameter('difference to base frequency', FloatRange(unit='Hz'), default=0)
|
||||
@ -128,55 +210,114 @@ class FrequencyDif(Readable):
|
||||
super().initModule()
|
||||
self.freq.register_dif(self)
|
||||
|
||||
def write_value(self, target):
|
||||
self.freq.write_target(target + self.base)
|
||||
return self.value # this was updated in Frequency
|
||||
|
||||
def read_value(self):
|
||||
return self.freq - self.base
|
||||
|
||||
|
||||
class Base(Collector):
|
||||
freq = Attached()
|
||||
adq = Attached(Adq)
|
||||
sr = Parameter('samples per record', datatype=IntRange(1, 1E9), default=16384)
|
||||
pollinterval = Parameter(datatype=FloatRange(0, 120)) # allow pollinterval = 0
|
||||
_data = None
|
||||
_data_args = None
|
||||
return self.freq.value - self.base
|
||||
|
||||
def read_status(self):
|
||||
adqstate = self.adq.get_status()
|
||||
if adqstate == Adq.BUSY:
|
||||
return BUSY, 'acquiring'
|
||||
if adqstate == Adq.UNDEFINED:
|
||||
return ERROR, 'no data yet'
|
||||
if adqstate == Adq.READY:
|
||||
return IDLE, 'new data available'
|
||||
return IDLE, ''
|
||||
|
||||
def get_data(self):
|
||||
data = self.adq.get_data(*self._data_args)
|
||||
if id(data) != id(self._data):
|
||||
self._data = data
|
||||
return data
|
||||
return None
|
||||
return self.freq.read_status()
|
||||
|
||||
|
||||
class PulseEcho(Base):
|
||||
value = Parameter("t, i, q, pulse curves",
|
||||
TupleOf(*[ArrayOf(FloatRange(), 0, 16283) for _ in range(4)]), default=[[]] * 4)
|
||||
class Base:
|
||||
freq = Attached()
|
||||
sr = Parameter('samples per record', datatype=IntRange(1, 1E9), default=16384)
|
||||
adq = None
|
||||
_rawsignal = None
|
||||
_fast_poll = 0.001
|
||||
|
||||
def shutdownModule(self):
|
||||
if self.adq:
|
||||
self.adq.deletecu()
|
||||
self.adq = None
|
||||
|
||||
@Command(argument=TupleOf(FloatRange(unit='ns'), FloatRange(unit='ns'), IntRange(0,99999)),
|
||||
result=TupleOf(FloatRange(),
|
||||
ArrayOf(ArrayOf(IntRange(-0x7fff, 0x7fff), 0, 99999))))
|
||||
def get_signal(self, start, end, npoints):
|
||||
"""get signal
|
||||
|
||||
:param start: start time (ns)
|
||||
:param end: end time (ns)
|
||||
:param npoints: hint for number of data points
|
||||
:return: (<time-step>, array of array of y)
|
||||
|
||||
for performance reasons the result data is rounded to int16
|
||||
"""
|
||||
# convert ns to samples
|
||||
sr = self.adq.sample_rate * 1e-9
|
||||
istart = round(start * sr)
|
||||
iend = min(self.sr, round(end * sr))
|
||||
nbin = max(1, round((iend - istart) / npoints))
|
||||
iend = iend // nbin * nbin
|
||||
return (nbin / sr,
|
||||
[np.round(ch[istart:iend].reshape((-1, nbin)).mean(axis=1)) for ch in self._rawsignal])
|
||||
|
||||
|
||||
class PulseEcho(Base, Readable):
|
||||
value = Parameter(default=0)
|
||||
nr = Parameter('number of records', datatype=IntRange(1, 9999), default=500)
|
||||
bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'), default=10E6)
|
||||
control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||
time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||
# curves = Attached(mandatory=False)
|
||||
pollinterval = Parameter('poll interval', datatype=FloatRange(0,120))
|
||||
|
||||
starttime = None
|
||||
_starttime = None
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.adq = Adq()
|
||||
self.adq.init(self.sr, self.nr)
|
||||
self.roilist = []
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
|
||||
def doPoll(self):
|
||||
try:
|
||||
data = self.adq.get_data()
|
||||
except Exception as e:
|
||||
self.status = ERROR, repr(e)
|
||||
return
|
||||
if data is None:
|
||||
if self.adq.busy:
|
||||
return
|
||||
self.adq.start(PEdata(self.adq))
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
return
|
||||
|
||||
roilist = [r for r in self.roilist if r.enable]
|
||||
freq = self.freq.read_value()
|
||||
if not freq:
|
||||
self.log.info('freq=0')
|
||||
return
|
||||
gates, curves = data.gates_and_curves(
|
||||
freq, (self.time, self.time + self.size),
|
||||
[r.interval for r in roilist], self.bw)
|
||||
for i, roi in enumerate(roilist):
|
||||
a = gates[i][0]
|
||||
b = gates[i][1]
|
||||
roi.value = a, b
|
||||
if roi.i:
|
||||
roi.i.value = a
|
||||
if roi.q:
|
||||
roi.q.value = b
|
||||
if roi.a:
|
||||
roi.a.value = math.sqrt(a ** 2 + b ** 2)
|
||||
if roi.p:
|
||||
roi.p.value = math.atan2(a, b) * 180 / math.pi
|
||||
self._curves = curves
|
||||
self._rawsignal = data.rawsignal
|
||||
|
||||
@Command(result=TupleOf(*[ArrayOf(FloatRange(), 0, 99999)
|
||||
for _ in range(4)]))
|
||||
def get_curves(self):
|
||||
"""retrieve curves"""
|
||||
return self._curves
|
||||
|
||||
def write_nr(self, value):
|
||||
self.adq.init(self.sr, value)
|
||||
@ -190,124 +331,170 @@ class PulseEcho(Base):
|
||||
def register_roi(self, roi):
|
||||
self.roilist.append(roi)
|
||||
|
||||
def go(self):
|
||||
self.starttime = time.time()
|
||||
self.adq.start()
|
||||
|
||||
def read_value(self):
|
||||
if self.get_rawdata(): # new data available
|
||||
roilist = [r for r in self.roilist if r.enable]
|
||||
freq = self.freq.value
|
||||
gates = self.adq.gates_and_curves(self._data, freq,
|
||||
(self.time, self.time + self.size),
|
||||
[r.interval for r in roilist])
|
||||
for i, roi in enumerate(roilist):
|
||||
roi.i = a = gates[i][0]
|
||||
roi.q = b = gates[i][1]
|
||||
roi.value = math.sqrt(a ** 2 + b ** 2)
|
||||
roi.phase = math.atan2(a, b) * 180 / math.pi
|
||||
return self.adq.curves
|
||||
|
||||
# TODO: CONTROL
|
||||
# inphase = self.roilist[0].i
|
||||
# if self.control:
|
||||
# newfreq = freq + inphase * self.slope - self.basefreq
|
||||
# # step = sorted((-self.maxstep, inphase * self.slope, self.maxstep))[1]
|
||||
# if self.old:
|
||||
# fdif = freq - self.old[0]
|
||||
# idif = inphase - self.old[1]
|
||||
# if abs(fdif) >= self.minstep:
|
||||
# self.slope = - fdif / idif
|
||||
# else:
|
||||
# fdif = 0
|
||||
# idif = 0
|
||||
# newfreq = freq + self.minstep
|
||||
# self.old = (freq, inphase)
|
||||
# if self.skipctrl > 0: # do no control for some time after changing frequency
|
||||
# self.skipctrl -= 1
|
||||
# elif self.control:
|
||||
# self.freq = sorted((self.freq - self.maxstep, newfreq, self.freq + self.maxstep))[1]
|
||||
CONTINUE = 0
|
||||
GO = 1
|
||||
DONE_GO = 2
|
||||
WAIT_GO = 3
|
||||
|
||||
|
||||
class RUS(Base):
|
||||
class RUS(Base, Collector):
|
||||
freq = Attached()
|
||||
imod = Attached(mandatory=False)
|
||||
qmod = Attached(mandatory=False)
|
||||
value = Parameter('averaged (I, Q) tuple', TupleOf(FloatRange(), FloatRange()))
|
||||
periods = Parameter('number of periods', IntRange(1, 9999), default=12)
|
||||
scale = Parameter('scale,taking into account input attenuation', FloatRange(), default=0.1)
|
||||
input_phase_stddev = Parameter('input signal quality', FloatRange(unit='rad'))
|
||||
output_phase_slope = Parameter('output signal phase slope', FloatRange(unit='rad/sec'))
|
||||
output_amp_slope = Parameter('output signal amplitude change', FloatRange(unit='1/sec'))
|
||||
phase = Parameter('phase', FloatRange(unit='deg'))
|
||||
amp = Parameter('amplitude', FloatRange())
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
periods = Parameter('number of periods', IntRange(1, 999999), default=12, readonly=False)
|
||||
input_delay = Parameter('throw away everything before this time',
|
||||
FloatRange(unit='ns'), default=10000, readonly=False)
|
||||
input_range = Parameter('input range (taking into account attenuation)', FloatRange(unit='V'),
|
||||
default=10, readonly=False)
|
||||
output_range = Parameter('output range', FloatRange(unit='V'),
|
||||
default=1, readonly=False)
|
||||
input_amplitude = Parameter('input signal amplitude', FloatRange(unit='V'), default=0)
|
||||
output_amplitude = Parameter('output signal amplitude', FloatRange(unit='V'), default=0)
|
||||
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||
amp = Parameter('amplitude', FloatRange(), default=0)
|
||||
continuous = Parameter('continuous mode', BoolType(), readonly=False, default=True)
|
||||
pollinterval = Parameter(datatype=FloatRange(0, 120), default=5)
|
||||
|
||||
starttime = None
|
||||
_data_args = None
|
||||
_starttime = None
|
||||
_iq = 0
|
||||
_wait_until = 0 # deadline for returning to continuous mode
|
||||
_action = CONTINUE # one of CONTINUE, GO, DONE_GO, WAIT_GO
|
||||
_status = IDLE, 'no data yet'
|
||||
_busy = False # waiting for end of aquisition (not the same as self.status[0] == BUSY)
|
||||
_requested_freq = None
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.adq = Adq()
|
||||
self.freq.addCallback('value', self.update_freq)
|
||||
self.freq.addCallback('target', self.update_freq_target)
|
||||
# self.write_periods(self.periods)
|
||||
|
||||
def read_value(self):
|
||||
if self._data_args is None:
|
||||
return self.value # or may we raise as no value is defined yet?
|
||||
data = self.get_data(RUSdata, *self._data_args)
|
||||
if data:
|
||||
# data available
|
||||
data.calc_quality()
|
||||
self.input_phase_stddev = data.input_stddev.imag
|
||||
self.output_phase_slope = data.output_slope.imag
|
||||
self.output_amp_slope = data.output_slope.real
|
||||
def update_freq_target(self, value):
|
||||
self.go()
|
||||
|
||||
iq = data.iq * self.scale
|
||||
def update_freq(self, value):
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
self._requested_freq = value
|
||||
self.freq.set_busy() # is only effective when the update was trigger within freq.write_target
|
||||
|
||||
def get_quality_info(self, data):
|
||||
"""hook for RESqual"""
|
||||
data.timer.show()
|
||||
|
||||
def doPoll(self):
|
||||
try:
|
||||
data = self.adq.get_data()
|
||||
except Exception as e:
|
||||
self.set_status(ERROR, repr(e))
|
||||
self._busy = False
|
||||
self._action = WAIT_GO
|
||||
self.wait_until = time.time() + 2
|
||||
return
|
||||
|
||||
if data: # this is new data
|
||||
self._busy = False
|
||||
self.get_quality_info(data) # hook for RUSqual
|
||||
self._rawsignal = data.rawsignal
|
||||
self.input_amplitude = data.inp.amplitude * self.input_range
|
||||
self.output_amplitude = data.out.amplitude * self.output_range
|
||||
self._iq = iq = data.iq * self.output_range / self.input_range
|
||||
self.phase = np.arctan2(iq.imag, iq.real) * 180 / np.pi
|
||||
self.amp = np.abs(iq.imag, iq.real)
|
||||
return iq.real, iq.imag
|
||||
return self.value
|
||||
self.amp = np.abs(iq)
|
||||
self.read_value()
|
||||
self.set_status(IDLE, '')
|
||||
if self.freq.isBusy():
|
||||
if data.freq == self._requested_freq:
|
||||
self.log.info('set freq idle %.3f', time.time() % 1.0)
|
||||
self.freq.set_idle()
|
||||
else:
|
||||
self.log.warn('freq does not match: requested %.14g, from data: %.14g',
|
||||
self._requested_freq, data.freq)
|
||||
else:
|
||||
self.log.info('freq not busy %.3f', time.time() % 1.0)
|
||||
if self._action == CONTINUE:
|
||||
self.setFastPoll(False)
|
||||
self.log.info('slow')
|
||||
return
|
||||
elif self._busy:
|
||||
if self._action == DONE_GO:
|
||||
self.log.info('busy')
|
||||
self.set_status(BUSY, 'acquiring')
|
||||
else:
|
||||
self.set_status(IDLE, 'acquiring')
|
||||
return
|
||||
if self._action == CONTINUE and self.continuous:
|
||||
print('CONTINUE')
|
||||
self.start_acquisition()
|
||||
self.set_status(IDLE, 'acquiring')
|
||||
return
|
||||
if self._action == GO:
|
||||
print('pending GO')
|
||||
self.start_acquisition()
|
||||
self._action = DONE_GO
|
||||
self.set_status(BUSY, 'acquiring')
|
||||
return
|
||||
if self._action == DONE_GO:
|
||||
self._action = WAIT_GO
|
||||
self._wait_until = time.time() + 2
|
||||
self.set_status(IDLE, 'paused')
|
||||
return
|
||||
if self._action == WAIT_GO:
|
||||
if time.time() > self._wait_until:
|
||||
self._action = CONTINUE
|
||||
self.start_acquisition()
|
||||
self.set_status(IDLE, 'acquiring')
|
||||
|
||||
def set_status(self, *status):
|
||||
self._status = status
|
||||
if self._status != self.status:
|
||||
self.read_status()
|
||||
|
||||
def read_status(self):
|
||||
return self._status
|
||||
|
||||
def read_value(self):
|
||||
if self.imod:
|
||||
self.imod.value = self._iq.real
|
||||
if self.qmod:
|
||||
self.qmod.value = self._iq.imag
|
||||
return self._iq.real, self._iq.imag
|
||||
|
||||
@Command
|
||||
def go(self):
|
||||
self.starttime = time.time()
|
||||
freq = self.freq.value
|
||||
self._data_args = (RUSdata, freq, self.periods)
|
||||
"""start acquisition"""
|
||||
self.log.info('go %.3f', time.time() % 1.0)
|
||||
if self._busy:
|
||||
self._action = GO
|
||||
else:
|
||||
self._action = DONE_GO
|
||||
self.start_acquisition()
|
||||
self._status = BUSY, 'acquiring'
|
||||
self.read_status()
|
||||
|
||||
def start_acquisition(self):
|
||||
self.log.info('start %.3f', time.time() % 1.0)
|
||||
freq = self.freq.read_value()
|
||||
self.sr = round(self.periods * self.adq.sample_rate / freq)
|
||||
self.adq.init(self.sr, 1)
|
||||
self.adq.start()
|
||||
self.read_status()
|
||||
delay_samples = round(self.input_delay * self.adq.sample_rate * 1e-9)
|
||||
self.adq.init(self.sr + delay_samples, 1)
|
||||
self.adq.start(RUSdata(self.adq, freq, self.periods, delay_samples))
|
||||
self._busy = True
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
|
||||
|
||||
class ControlLoop:
|
||||
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000)
|
||||
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000)
|
||||
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6)
|
||||
class RUSqual(RUS):
|
||||
"""version with additional info about quality of input and output signal"""
|
||||
|
||||
input_phase_stddev = Parameter('input signal quality', FloatRange(unit='rad'), default=0)
|
||||
output_phase_slope = Parameter('output signal phase slope', FloatRange(unit='rad/sec'), default=0)
|
||||
output_amp_slope = Parameter('output signal amplitude change', FloatRange(unit='1/sec'), default=0)
|
||||
|
||||
# class Frequency(HasIO, Readable):
|
||||
# pars = Attached()
|
||||
# curves = Attached(mandatory=False)
|
||||
# maxy = Property('plot y scale', datatype=FloatRange(), default=0.5)
|
||||
#
|
||||
# value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0)
|
||||
# basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
# nr = Parameter('number of records', datatype=IntRange(1,10000), default=500)
|
||||
# sr = Parameter('samples per record', datatype=IntRange(1,1E9), default=16384)
|
||||
# freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
# bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'),default=10E6)
|
||||
# amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False)
|
||||
# control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||
# rusmode = Parameter('RUS mode on?', BoolType(), readonly=False, default=False)
|
||||
# time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
# readonly=False)
|
||||
# size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
# readonly=False)
|
||||
# pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||
# maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
# default=10000)
|
||||
# minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
# readonly=False, default=4000)
|
||||
# slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
# default=1e6)
|
||||
# plot = Parameter('create plot images', BoolType(), readonly=False, default=True)
|
||||
# save = Parameter('save data', BoolType(), readonly=False, default=True)
|
||||
# pollinterval = Parameter(datatype=FloatRange(0,120))
|
||||
def get_quality_info(self, data):
|
||||
qual = data.get_quality()
|
||||
self.input_phase_stddev = qual.input_stddev.imag
|
||||
self.output_phase_slope = qual.output_slope.imag
|
||||
self.output_amp_slope = qual.output_slope.real
|
||||
|
@ -5,6 +5,7 @@ which should be in the branch where logdif.py is running
|
||||
"""
|
||||
|
||||
import sys
|
||||
from readchar import readchar
|
||||
from subprocess import check_output
|
||||
|
||||
branches = sys.argv[1:3]
|
||||
@ -91,12 +92,13 @@ def print_commit(line):
|
||||
output.append(f'{no:3}:{iline[0]}')
|
||||
iline[1] = '' # clear title
|
||||
else:
|
||||
output.append(' ' * 11)
|
||||
output.append(' ' * 12)
|
||||
if visible:
|
||||
print(' '.join(output), title)
|
||||
cnt[0] += 1
|
||||
if cnt[0] % 50 == 0:
|
||||
if input(f' {br0:11s} {br1:11s}'):
|
||||
print(f' {br0:12s} {br1:12s}--- press any letter to continue, return to stop ---')
|
||||
if readchar() in 'q\n':
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
|
76
test/test_all_modules.py
Normal file
76
test/test_all_modules.py
Normal file
@ -0,0 +1,76 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""tests for probable implementation errors."""
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
from glob import glob
|
||||
import pytest
|
||||
from frappy.core import Module, Drivable
|
||||
from frappy.errors import ProgrammingError
|
||||
|
||||
|
||||
all_drivables = set()
|
||||
for pyfile in glob('frappy_*/*.py') + glob('frappy/*.py'):
|
||||
module = pyfile[:-3].replace('/', '.')
|
||||
try:
|
||||
importlib.import_module(module)
|
||||
except Exception as e:
|
||||
print(module, e)
|
||||
continue
|
||||
for obj_ in sys.modules[module].__dict__.values():
|
||||
if isinstance(obj_, type) and issubclass(obj_, Drivable):
|
||||
all_drivables.add(obj_)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('modcls', all_drivables)
|
||||
def test_stop_doc(modcls):
|
||||
# make sure that implemented stop methods have a doc string
|
||||
if (modcls.stop.description == Drivable.stop.description
|
||||
and modcls.stop.func != Drivable.stop.func):
|
||||
assert modcls.stop.func.__doc__ # stop method needs a doc string
|
||||
|
||||
|
||||
def test_bad_method_test():
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod1(Module): # pylint: disable=unused-variable
|
||||
def read_param(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod2(Module): # pylint: disable=unused-variable
|
||||
def write_param(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Module): # pylint: disable=unused-variable
|
||||
def do_cmd(self):
|
||||
pass
|
||||
|
||||
# no complain in this case
|
||||
# checking this would make code to check much more complicated.
|
||||
# in the rare cases used it might even be intentional
|
||||
class Mixin:
|
||||
def read_param(self):
|
||||
pass
|
||||
|
||||
class ModTest(Mixin, Module): # pylint: disable=unused-variable
|
||||
pass
|
@ -23,8 +23,6 @@
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import importlib
|
||||
from glob import glob
|
||||
import pytest
|
||||
|
||||
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||
@ -33,6 +31,7 @@ from frappy.modules import Communicator, Drivable, Readable, Module, Writable
|
||||
from frappy.params import Command, Parameter, Limit
|
||||
from frappy.rwhandler import ReadHandler, WriteHandler, nopoll
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.properties import Property
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
@ -319,17 +318,17 @@ def test_command_inheritance():
|
||||
"""third"""
|
||||
|
||||
assert Sub1.accessibles['cmd'].for_export() == {
|
||||
'description': 'first', 'group': 'grp', 'visibility': 2,
|
||||
'description': 'first', 'group': 'grp', 'visibility': 'ww-',
|
||||
'datainfo': {'type': 'command', 'argument': {'type': 'bool'}}
|
||||
}
|
||||
|
||||
assert Sub2.accessibles['cmd'].for_export() == {
|
||||
'description': 'second', 'group': 'grp', 'visibility': 2,
|
||||
'description': 'second', 'group': 'grp', 'visibility': 'ww-',
|
||||
'datainfo': {'type': 'command', 'result': {'type': 'bool'}}
|
||||
}
|
||||
|
||||
assert Sub3.accessibles['cmd'].for_export() == {
|
||||
'description': 'third', 'visibility': 2,
|
||||
'description': 'third', 'visibility': 'ww-',
|
||||
'datainfo': {'type': 'command', 'result': {'type': 'double'}}
|
||||
}
|
||||
|
||||
@ -382,7 +381,7 @@ def test_command_check():
|
||||
'cmd': {'argument': {'type': 'double', 'min': 1, 'max': 0}},
|
||||
}, srv)
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
with pytest.raises(ConfigError):
|
||||
BadDatatype('o', logger, {
|
||||
'description': '',
|
||||
'cmd': {'visibility': 'invalid'},
|
||||
@ -447,6 +446,15 @@ def test_override():
|
||||
# inherit doc string
|
||||
assert Mod2.stop.description == Mod.stop.description
|
||||
|
||||
class Base(Module):
|
||||
attr = Property('test property', FloatRange())
|
||||
|
||||
class Subclass(Base):
|
||||
attr = Parameter('test parameter', FloatRange())
|
||||
|
||||
class SubSubclass(Subclass): # pylint: disable=unused-variable
|
||||
attr = 5.0
|
||||
|
||||
|
||||
def test_command_config():
|
||||
class Mod(Module):
|
||||
@ -922,27 +930,6 @@ def test_interface_classes(bases, iface_classes):
|
||||
assert m.interface_classes == iface_classes
|
||||
|
||||
|
||||
all_drivables = set()
|
||||
for pyfile in glob('frappy_*/*.py'):
|
||||
module = pyfile[:-3].replace('/', '.')
|
||||
try:
|
||||
importlib.import_module(module)
|
||||
except Exception as e:
|
||||
print(module, e)
|
||||
continue
|
||||
for obj_ in sys.modules[module].__dict__.values():
|
||||
if isinstance(obj_, type) and issubclass(obj_, Drivable):
|
||||
all_drivables.add(obj_)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('modcls', all_drivables)
|
||||
def test_stop_doc(modcls):
|
||||
# make sure that implemented stop methods have a doc string
|
||||
if (modcls.stop.description == Drivable.stop.description
|
||||
and modcls.stop.func != Drivable.stop.func):
|
||||
assert modcls.stop.func.__doc__ # stop method needs a doc string
|
||||
|
||||
|
||||
def test_write_error():
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
@ -26,7 +26,7 @@ import pytest
|
||||
|
||||
from frappy.datatypes import BoolType, FloatRange, IntRange, StructOf
|
||||
from frappy.errors import ProgrammingError
|
||||
from frappy.modulebase import HasAccessibles
|
||||
from frappy.modulebase import HasAccessibles, Module
|
||||
from frappy.params import Command, Parameter
|
||||
|
||||
|
||||
@ -149,3 +149,105 @@ def test_update_unchanged_ok(arg, value):
|
||||
def test_update_unchanged_fail(arg):
|
||||
with pytest.raises(ProgrammingError):
|
||||
Parameter('', datatype=FloatRange(), default=0, update_unchanged=arg)
|
||||
|
||||
|
||||
def make_module(cls):
|
||||
class DispatcherStub:
|
||||
def announce_update(self, moduleobj, pobj):
|
||||
pass
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
class ServerStub:
|
||||
dispatcher = DispatcherStub()
|
||||
secnode = None
|
||||
|
||||
return cls('test', LoggerStub(), {'description': 'test'}, ServerStub())
|
||||
|
||||
|
||||
def test_optional_parameters():
|
||||
class Base(Module):
|
||||
p1 = Parameter('overridden', datatype=FloatRange(),
|
||||
default=1, readonly=False, optional=True)
|
||||
p2 = Parameter('not overridden', datatype=FloatRange(),
|
||||
default=2, readonly=False, optional=True)
|
||||
|
||||
class Mod(Base):
|
||||
p1 = Parameter()
|
||||
|
||||
def read_p1(self):
|
||||
return self.p1
|
||||
|
||||
def write_p1(self, value):
|
||||
return value
|
||||
|
||||
assert Base.accessibles['p2'].optional
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod2(Base): # pylint: disable=unused-variable
|
||||
def read_p2(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Base): # pylint: disable=unused-variable
|
||||
def write_p2(self):
|
||||
pass
|
||||
|
||||
base = make_module(Base)
|
||||
mod = make_module(Mod)
|
||||
|
||||
assert 'p1' not in base.accessibles
|
||||
assert 'p1' not in base.parameters
|
||||
assert 'p2' not in base.accessibles
|
||||
assert 'p2' not in base.parameters
|
||||
|
||||
assert 'p1' in mod.accessibles
|
||||
assert 'p1' in mod.parameters
|
||||
assert 'p2' not in mod.accessibles
|
||||
assert 'p2' not in mod.parameters
|
||||
|
||||
assert mod.p1 == 1
|
||||
assert mod.read_p1() == 1
|
||||
mod.p1 = 11
|
||||
assert mod.read_p1() == 11
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
assert mod.p2
|
||||
with pytest.raises(AttributeError):
|
||||
mod.read_p2()
|
||||
with pytest.raises(ProgrammingError):
|
||||
mod.p2 = 2
|
||||
with pytest.raises(AttributeError):
|
||||
mod.write_p2(2)
|
||||
|
||||
|
||||
def test_optional_commands():
|
||||
class Base(Module):
|
||||
c1 = Command(FloatRange(1), result=FloatRange(2), description='overridden', optional=True)
|
||||
c2 = Command(description='not overridden', optional=True)
|
||||
|
||||
class Mod(Base):
|
||||
def c1(self, value):
|
||||
return value + 1
|
||||
|
||||
base = make_module(Base)
|
||||
mod = make_module(Mod)
|
||||
|
||||
assert 'c1' not in base.accessibles
|
||||
assert 'c1' not in base.commands
|
||||
assert 'c2' not in base.accessibles
|
||||
assert 'c2' not in base.commands
|
||||
|
||||
assert 'c1' in mod.accessibles
|
||||
assert 'c1' in mod.commands
|
||||
assert 'c2' not in mod.accessibles
|
||||
assert 'c2' not in mod.commands
|
||||
|
||||
assert mod.c1(7) == 8
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
mod.c2()
|
||||
|
Loading…
x
Reference in New Issue
Block a user