147 Commits

Author SHA1 Message Date
38b8a7a118 frappy.client.SecopClient: fix setParameterFromString
add missing datatype.export_value

Change-Id: I824d922fdda1ab548e4625982e1485ec4b758cb9
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36172
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-27 15:56:51 +02:00
d73b8bc774 frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-27 15:56:51 +02:00
ceb6e487a8 frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-27 15:56:51 +02:00
eb08e31b62 frappy.client.SecopClient: fix setParameterFromString
add missing datatype.export_value

Change-Id: I824d922fdda1ab548e4625982e1485ec4b758cb9
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36172
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-27 15:56:51 +02:00
4b88ece3be frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-27 15:56:51 +02:00
c22e41c8e2 frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-27 15:56:51 +02:00
701d1f1735 frappy.client.SecopClient: fix setParameterFromString
add missing datatype.export_value

Change-Id: I824d922fdda1ab548e4625982e1485ec4b758cb9
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36172
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-27 15:56:51 +02:00
400b314fc7 frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-27 15:56:51 +02:00
ce3b3ddfd6 frappy-psi.logo: fix IO.communicate reply 2025-05-27 15:51:17 +02:00
3045a11ff9 dil5/logo: fix valve addresses 2025-05-27 15:51:17 +02:00
de773f084d frappy_psi.logo: add comLog
+ fix bug in frappy_psi/dilution_statemachine.py
2025-05-27 15:51:17 +02:00
564bea63e4 DIL5: improve states in the state machine 2025-05-27 15:51:17 +02:00
bd8a417112 use longer, unique uris for USB serial connections 2025-05-27 15:51:17 +02:00
e6263cbcd0 frappy_psi.pfeiffer_new: calculate crc in PfeifferProtocol 2025-05-27 15:51:17 +02:00
7c06bbeec2 haake: fix haake2 and add pumprunning 2025-05-27 14:48:16 +02:00
d6947711c6 increase omit_unchanged_within to 60 s
+ fix a bug reading this from general config file
2025-05-22 08:35:59 +02:00
0938049e89 [WIP] further work on CCU4
Change-Id: Icdd6e253f276a5ade44fb4103306160fc348556b
2025-05-22 08:35:59 +02:00
0053b7847c frappy.client.interactive: no pathlib needed here
allow the argument of the run function to be a simple string

Change-Id: I1d5de3802b6adc22a01a262d4cb017bf7dd084c5
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36343
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
5c93953183 config: add 'include' and 'override'
including a config file and overriding some properties is
helpful when we do not want to modify the original config
but run it with sligthly different properties.

this involves some redesign a.o.:
- modules are collected in a dict instead of a list in
  order for 'override' to find the related module
- checking for duplicates happens in the Collector

Do not warn when included file does not end with '_cfg.py',
as this may be intentional, in case a file is only used
via 'include' and not as cfg file alone.

+ remove unused method Collector.append
+ complain with specific error message when Node is not given

Change-Id: Id568f04d6d84622ef2547412eb6f288fcebf986f
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36357
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-22 08:35:59 +02:00
b60ad38fd0 [deb] Release v0.20.5 2025-05-22 08:35:59 +02:00
7b0356b4ee frappy.client.interactive: improve updates while driving
- instead to show first current 'value' and 'status', and then
  the changes, show changes only - this way updates appear
  in the expected order
- for this SecopClient.register_callback needs a 'callimmediately'
  argument

Change-Id: I3e91c2c15bca7fee2eba3b1bf1dd27313da3ae29
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36291
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
7faf328141 client: add SecopClient.execCommandFromString
analogous to setParameterFromString
to be used in simple clients where values of structured datatypes
are just python literals

Change-Id: I4936a3c5c0cac45bd7e875c938ce2700b4f4a979
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36262
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
d07406c2fd frappy_psi/ls370res: various bug fixes
- avoid some error messages on disabled channels
- update value of channel switcher properly
- fix bug in set_param method

Change-Id: I16f5cf2d7abce0c0fae17266f0c8e949e8416b7a
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36263
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
dbd4667c87 frappy.client.SecopClient: fix setParameterFromString
add missing datatype.export_value

Change-Id: I824d922fdda1ab548e4625982e1485ec4b758cb9
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36172
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-22 08:35:59 +02:00
6e7514b718 installer: add recipe to build macOS app bundle
Change-Id: Ief4401626db293fecc37f1d1ec72cf295b55fccf
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36060
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
2025-05-22 08:35:59 +02:00
c49f15ce64 Add DIL5 Statemachine and LOGO 2025-05-22 08:35:59 +02:00
3bae6f8d7f [WIP] fi furnace improvements
- still under development

Change-Id: I5fc22f041fb136b549016f510f06ea703122bee5
2025-05-22 08:35:59 +02:00
e46291eef6 change prot for dummy to 5000
Change-Id: If35bbe6783fe133c0c6c87f402ba70aec00fa964
2025-05-22 08:35:59 +02:00
af746f59bb dummy_cfg.py: add test case with big enum
+ fix undefined value in frappy_psi.ls370res.Switcher

Change-Id: I59f2814b945533c487999f9af638e0fb2040e862
2025-05-22 08:35:59 +02:00
a2a4d4332a more demo test cases
- added them to cfg/dummy_cfg.py
+ treat enum correctly in SecopClient.setParameterFromString

Change-Id: Ia5b2d8d3a21c3215cb93d90975086eb9995b1543
2025-05-22 08:35:59 +02:00
aff6a2381a logdif.py: use single key to stop or continue
Change-Id: I53b3254074eda7491dd16bbc39168960b0980e39
2025-05-22 08:35:59 +02:00
eb8eee02d1 follow-up change to 35931: make Proxy a Module
Proxy must be a class, because of the new check for configured
'cls' inheriting from Module. Use Proxy.__new__ to implement
this.

Change-Id: I4bb036afc2ce92187a9049dff0a6f22b20c3a260
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36104
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
2025-05-22 08:35:59 +02:00
9a818ab40b revert 'change to new visibility spec'
nicos is not yet ready for this

Change-Id: Ibfbb7e32e06a6e0616ded8342bc5844fd531944f
2025-05-22 08:35:59 +02:00
dc87a50c76 add sr830 addons cfg 2025-05-22 08:35:59 +02:00
5974d759b0 frappy_psi.parmod: extract a tuple element as own moudle
Change-Id: I8d904ed21f8a5c16ae71daf30c9a1ea42876b451
2025-05-22 08:35:59 +02:00
396bbe9982 frappy_psi.ionopimax.AnalogInput: set value range on datainfo
Change-Id: I1e3da956e829f69a0af416b7beadb81bd6bc0cb1
2025-05-22 08:35:59 +02:00
ae98fa174c frappy_psi.furnace: special classes PTXgauge and PRtransmitter
move some initialization from cfg file to source code
+ make 'out of calibrated range' and 'sensor break' more generic

Change-Id: I3e92100fdb9c983f82665de9d8e063609cd7af5a
2025-05-22 08:35:59 +02:00
83ad7b1638 change to new visibility spec
+ visibiliy is no longer an EnumType, as this would break
  the specs

Change-Id: I1197c82f31c33c210fdcda0b49a0c38027880d77
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36088
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
257b24a480 make sure unexported modules are initialized
take the opportunity for a small redesign:

- create a new method build_descriptive_data which
  calls secnode.get_modules also on unexported modules.

+ cache descriptive data

Change-Id: I4a0b8ac96108463dc0c800bb11a404206c26b092
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36089
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
2025-05-22 08:35:59 +02:00
29f0e18c1e better dummy server for seaweb tests
- new config file dummy
- frappy_demo.test.Temp now creates WARN and ERROR status
  and may be disabled

Change-Id: Ibc7bb565f18c2c12cdc2a77bea1ee1bf1cc8bd41
2025-05-22 08:35:59 +02:00
144083a54a ionopimax: bug fixes 2025-05-22 08:35:59 +02:00
748a39f932 frappy_psi.ionopimax redesign
Change-Id: I46b62522c24ad9f0352ba7a784d39ffd1cb79ef3
2025-05-22 08:35:59 +02:00
8cea974f49 [WIP] further fixes for linse-fi 2025-05-22 08:35:59 +02:00
5768f096a5 frappy-play: fix import order 2025-05-22 08:35:59 +02:00
94859fe2ef [WIP] fixes for linse-fi
Change-Id: Iac28e9654a764331cd903896879834cd6127a919
2025-05-22 08:35:59 +02:00
fcf867675e fixes on picontrol and tdkpower
Change-Id: Ia891e7df23d8408b857dac795ed0ad9973ccf993
2025-05-22 08:35:59 +02:00
09dce1aabd fixes on small furnace 2025-05-22 08:35:59 +02:00
eefe271cba add fi_cfg.py (ILL furnace)
Change-Id: I8720dbeb3f29b07eaeae59558c58b2fa87096dc9
2025-05-22 08:35:59 +02:00
c42dd41dec WIP frappy_psi/tdkpower
Change-Id: I80d1beb0fae2a1cdd2aa5fabc5d31c651c2cb3e7
2025-05-22 08:35:59 +02:00
c545e36633 peus-plot: optionally give x-range as 2nd argument
Change-Id: I445ef00487fc34343f5a0333643f61753a1c4948
2025-05-22 08:35:59 +02:00
ef985f094c ultrasound.ControlRoi: fix control mechanism
Change-Id: I9d01de260d3bdc63eb1004ba4f714d38d1c0508c
2025-05-22 08:35:59 +02:00
55d399241c cfg/ls370test update 2025-05-22 08:35:59 +02:00
aea56bb16a improve error messages on module creation
- add name when target and value datatype are incompatible
- check that module class inherits from Module

Change-Id: I4edbdff1c250b64b74b1adf7287f9659dff69b26
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35931
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
3b8f3586cf fix overriding Parameter with value
a property declared in a base class may be overriden
with a parameter in a subclass. this is already allowed.
if then, in the subsubclass it is overridden by a bare value,
it fails.

Patchset 1: add a test for this
Patchset 4: add the fix

Change-Id: Ia5a26076a9ee98439932643a03878342d56f8396
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35932
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
56bee015b3 ultrasound.PE: fix control loop
Change-Id: I2b2bdf4ad48887ae256a68119f53e6a38048ce1e
2025-05-22 08:35:59 +02:00
1febb7bacf [WIP] ccracks / ccu4: split ccu() into nv(), he() and flow()
Change-Id: I346330a5f350bf03eefe86c8e890b59afaaaa231
2025-05-22 08:35:59 +02:00
df4dec6db5 frappy_psi.sensirion: fix a typo
Change-Id: I259151b7a1b908c8289ecb88d2d3d4e6d9e45c12
2025-05-22 08:35:59 +02:00
7feee607e0 ultrasound.PulseEcho: fix issue with roi intervals
now it should be time +- 0.5 * size
2025-05-22 08:35:59 +02:00
7c938a3bb1 frappy_psi/adq_mr (ultrasound): exit on reboot error message
otherwise the error message is confusing
+ remove CR from line endings in adq_mr.py

Change-Id: Ia465a26803a92677383969ff620ef35e58f1a5ec
2025-05-22 08:35:59 +02:00
904dd96f95 ls370res: do not raise in read_rdgrng error when channel is disabled
Change-Id: I565e5cd74cf7f12bfd5eea9e8867117154461017
2025-05-22 08:35:59 +02:00
04fd8743a2 ah2700: make loss its own module 2025-05-22 08:35:59 +02:00
b59a98e4dc bin/us-plot: fix usage message 2025-05-22 08:35:59 +02:00
285c5c330f ultrasound: change control roi0 to a Readable (2)
+ remove cfg/PEUS.py
+ fix equipment_id of PEUS
+ add header to frappy_psi.iqplot
2025-05-22 08:35:59 +02:00
e3e27881d3 ultrasound: change control roi0 to a Readable
+ remove cfg/PEUS.py
+ fix equipment_id of PEUS
2025-05-22 08:35:59 +02:00
d26434f797 improve ultrasound plot clients
- make plot window not to raise to the front on replot
- chmod +x
2025-05-22 08:35:59 +02:00
c3b6aca7bc ultrasound: reworked after tests
- new classes in frappy_psi/ultrasound.py and frappy_psi/adq.mr.py
- add signal plottter
- move clients to bin/ directory

Change-Id: I8db8e5ebc082c346278f09e0e54504e070655f14
2025-05-22 08:35:59 +02:00
71372a450b gas10k / mercury.HeaterUpdate: switch off loop on startup
the class frappy_psi.mercury.HeaterUpdate is used for the output
of a soft pid loop. set target to 0 to switch off the loop
on startup.
2025-05-22 08:35:59 +02:00
79dbfdfad0 further work on needle valve, pump and lakeshore 2025-05-22 08:35:59 +02:00
5c7fe37807 change again how to exit logdif.py
Change-Id: I442ca8c2ee7ca25ff98a0e84df2688a55a0dcec9
2025-05-22 08:35:59 +02:00
e75b8b0b10 stop poller threads on shutdown: cosmetics
cosmetics after gerrit

Change-Id: I4d982f83e3fe5a8c8c821ac718e51b9a58de2a62
2025-05-22 08:35:59 +02:00
8d62375483 frappy_psi.ultrasound: add input_delay and other improvments
Change-Id: I6cb5690d82d96d6775fcb649fc633c4039932463
2025-05-22 08:35:59 +02:00
7c60daa568 frappy_psi.ultrasound: after rework (still wip)
Change-Id: I200cbeca2dd0f030a01a78ba4d38c342c3c8c8e3
2025-05-22 08:35:59 +02:00
fdc868c2d7 frappy-scan: resolve ip numbers to names
Change-Id: I07bf7c274aeb52f2aaa58e8aa2f3bcb2788556ee
2025-05-22 08:35:59 +02:00
b66acd4d73 stop poller threads on shutdown
make sure module methods are not called after shutdownModule

+ fix: when mod.enablePoll is False, pollInfo is None
  therefore we have to check before access

Change-Id: I83b28607b25996376939175be8abf0c5b27bcac1
2025-05-22 08:35:59 +02:00
da24c6244e frappy_psi.drums: changes after test
when trying with Marcel, we needed these fixes
2025-05-22 08:35:59 +02:00
738c4c7a51 config: Mod() should return config dict
this helps for coded configuration

Change-Id: I07bdf72f77082f31ee86192faec63df706dcbf56
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35803
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-22 08:35:59 +02:00
93fa2c818b config: validate value and default of parameters
The Parameter Properties 'value', 'default' and 'constant'
have ValueType, so they are not checked in the setProperty call.
We have to do this explicitly in Module._add_accessible.

Change-Id: I1e35adf2fe539411b4aebacd813adb07497de95b
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35797
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-22 08:35:59 +02:00
88eb6e93bd remove wrong <weight> from fonts on Qt6
Change-Id: Ib94b2ed74598b9f54c2361e61bfa940e60bd7c62
2025-05-22 08:35:59 +02:00
5242c903ae debian: update compat
Change-Id: I172dff4e0239ce90fe7b1c19fc800ba98f116270
2025-05-22 08:35:59 +02:00
4aec05c86b core: simplify test for methods names
The test for method names 'read_<param>' and 'write_<param>'
without a defined parameter is simplified. We do not check
anymore method names from base classes. Base classes
inheriting from HasAccessible are checked anyway at the
place they are defined.

+ add a test for it
+ move some tests to a new file test_all_modules.py, as
  test_modules.py is getting too long
+ fix missing doc string (frappy.simulation.SimDrivable.stop)

Change-Id: Id8a9afe5c977ae3b1371bd40c6da52be2fc79eb9
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35503
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-05-22 08:35:59 +02:00
a43590a3ff core: alternative approach for optional accessibles
This is meant to replace change 33375.
Optional commands and parameters may be declared with the argument
optional=True. In principle, optional commands are not really needed
to be declared, but doing so is nice for documentation reasons
and for inherited accessible properties.

Optional parameters and commands can not be used and are not
exported als long as they are not overridden in subclasses.

- add a test for this
+ fix an issue with checking for methods like read_<param> without
  <param> being a parameter

Change-Id: Ide5021127a02778e7f2f7162555ec8826f1471cb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35495
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
2025-05-22 08:35:59 +02:00
76e46f7c84 merged changes for lakeshore and ccu4 2025-05-22 08:35:59 +02:00
baf55baffc improve error handling 2025-05-22 08:35:59 +02:00
f7f77b168f gui: add org- and app-name to QtApplication
for a better path name of stored configuration
2025-05-22 08:35:59 +02:00
ebcf95e31e frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-05-22 08:35:59 +02:00
ee29bea821 add units to jtccr 2025-05-21 11:57:18 +02:00
4fc4cd3687 haake: make 2 versions depending on extra T sensor 2025-05-19 11:34:12 +02:00
1145493e81 varioxb: uncomment motor setup with triple quote 2025-05-08 09:07:31 +02:00
3f7ef438e3 ori4: revert removing '.' from rel_path
not sure if this is correct
2025-05-08 09:03:45 +02:00
90dc5359b9 dil2/3/4: make He3 pump visible 2025-05-08 08:59:46 +02:00
6d63c4e0df frappy_psi.phytron: improve clear_errors message 2025-03-20 12:53:15 +01:00
98fa19ce3b WIP new version of ultrasound
Change-Id: Iadb83396a64e277f6f0a37f7a96d92105648c4fe
2025-02-13 09:39:49 +01:00
7f83f76d38 frappy_demo.test: add parameter for testing error messages
Change-Id: Ifbf9d6829be373417d3bf1ff398d2aee283d8c9a
2025-02-13 09:39:49 +01:00
0ab849d0cf config: do not override equipment_id with name
In the previous code, the equipment_id was overridden by the
server name when the interface argument was given over
the commandline. This was leftover from the previous config
file format, where the config files not neccessarly needed
an equipment_id.

Change-Id: I2fc248372a7d2f61cc0690804268d6d066a0a9fa
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35391
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-02-13 09:39:49 +01:00
8ee49caba5 equipment_id for merged configs and routed nodes
Add a new custom module property 'original_id' indicating
the equipment_id the modules originally belongs to.
This property is only given, when distinct from the equipment_id
of the SEC node.
It happens when multiple config files are given, for all modules
but the ones given in the first file, and for routed modules,
when  multiple nodes are routed or own modules are given.

+ fix an issue in router: additional modules were ignore in case
of a single node.

+ small cosmetic changes in config.py reducing IDE complains

Change-Id: If846c47a06158629cef807d22b91f69e4f416563
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35396
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-02-13 09:39:49 +01:00
b1de9218bd take over changes from ultrasound PC
Change-Id: I1eae717a5963e618d87ddf52db991d428a046d24
2025-02-13 09:39:49 +01:00
8eaad86b66 WIP: old oxford devices (ILM, IPS, IGH...)
Change-Id: I4ca0dc6149d257818d300db4d886a1e33e8210be
2025-02-13 09:39:49 +01:00
85400a2777 move start_ramp_to_target to SimpleMagfiield
Change-Id: Iab3fe8738c560bf5ac2f11a4a34428a8ffd6a7c2
2025-02-13 09:39:49 +01:00
dda4afbe5b frappy_psi.ccu4: some smaller updates
Change-Id: I128ac57aad951fd8ad3bdf663c69c85644063645
2025-02-13 09:39:49 +01:00
9b079ddf4b make UPD listener work when 'tcp://' is omitted on interface
'tcp://' may be omitted on interfaces
add missing 'tcp://' earlier in code, so we do not need to check
for missing 'tcp://' again.

Change-Id: Ie9b4dbd168aebdb6edfe71dbd2cfc25e9229fe67
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35321
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2025-02-13 09:39:49 +01:00
898da75b89 fix bug in change 35001 (better error message)
fix bug in error message

Change-Id: I8151d20f840818fc26d42348f73e740cdb20e03d
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35287
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-02-13 09:39:49 +01:00
a7a846dfba frappy_psi.sea: fix case when bool is implemented as text
introduce SeaBool for this

Change-Id: I9c6b6ee7d33f11b173d612efc044fce8a563f626
2025-02-13 09:39:49 +01:00
6da671df62 an error on a write must not send an error update
Change-Id: I07a991bcf26e87121160a2e604f8842eba23ebaf
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35281
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-02-13 09:39:49 +01:00
bdb14af4af pylint: increase max number of positional arguments
Change-Id: Id88270b3c3c1efb56f47def733c1e9c745f1ab18
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35282
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
2025-02-13 09:39:11 +01:00
e57ad9826e better message when a parameter is overridden by an invalid value
happens e.g. then writing status = StatusType(...) instead of
status = Parameter(datatype=StatusType(...)) on the class level

+ improve doc strings

Change-Id: I05a0b0b0da4438a40b525da40018bb5b09fd5303
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35001
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-02-13 09:39:11 +01:00
8775103bf8 frappy_psi.pulse: fix fatal errors
even when a module is work in progress, it should not raise
an error on import

Change-Id: I2f91301ba2b0c574ea344c36a74da0f893aa326d
2025-02-13 09:39:11 +01:00
5636a76152 adapt temperature and temperature_regulation importance
- temperature_regulation on VTI should have higher importance (27)
  than temperature on sample stick, when Drivable (25)
2025-02-13 09:39:11 +01:00
745cc69e9e sea cfg: set visibility of calibration points to expert 2025-02-13 09:39:11 +01:00
b4c0a827f0 ma7: use new config type with sea_path and frappy.sea.LscDrivable 2025-02-13 09:39:11 +01:00
d57416a73e frappy_psi.sea: more improvements
- add sea_path property
- add LscDrivable (config of these modules is easier to understand)

Change-Id: I616dc94de6e784f6d8cfcf080d9a8408cbf73d93
2025-02-13 09:39:11 +01:00
8dcf6ca658 sea: fix parameter name mapping
- rel_path = ['tm', '.', 'set'] should mean:

'tm': tm parameters first, with /<obj?/tm as main value
'.': then all parameters directly at top level, except 'set'
'set': all parameters  below 'set'
driving happens at object level

- better name mangling (the 1st appearance of the same shortname
  is kept short)
2025-02-13 09:39:11 +01:00
bc66a314c4 logdif.py: leave on every input except bare return
Change-Id: I3d53c7b45fb9ef09a61be5af13a2cdc4d32d5c7d
2025-02-13 09:38:20 +01:00
6fac63d769 frappy.server: remove comment about opts in SecNode/Dispatcher
The options given in the node configuration may be used
for both SecNode (equipment_id) and Dispatcher (when the
frappy.protocol.router.Router is used as dispatcher).
It is correct that both remove the options known to them.

Change-Id: I2a34073e4e5490dcf8db577d9cb74788c0cb657b
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34989
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-11-28 18:04:26 +01:00
e41692bf2c frappy.server: use server name for SecNode name
no need to configure the name of SecNode and Dispatcher

Change-Id: I5199bbd77c74e4fe56b527a5a565a8285b0d831e
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34988
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:04:26 +01:00
dff3bd2f24 frappy.server bug fix: server name must not be a list
followup error from change 34893
this bug appears in HasComlog, only when comlog is switched on

Change-Id: Ic0db5ae0b0af9981b0c91ebacf2eb6cd704aaa58
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34987
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-11-28 18:04:26 +01:00
b67e5a9260 updated sync_branches for sinq branch
Change-Id: Ic3330c4049b527dc98704fbbd94180dcd4930cb1
2024-11-28 18:04:26 +01:00
4815f4e6b4 follow up change for 'better order of accessibles' (34904)
slight change to make it compatible with py 3.6/3.7, where
reversed(<dict>) was not allowed.

Change-Id: Id440870b5523a866b3afb470ba5db9cd6a9bb0ec
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35002
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-11-28 18:04:26 +01:00
e8ec9b415a improve lakeshore demo
use super call for read_status

TODO: update tutorial!
Change-Id: I2dd5631908dc370c6e6286587099e25a0e5ee867
2024-11-28 18:03:37 +01:00
5b9e36180e frappy_psi.bkpower: improve doc
Change-Id: I0736d1d8a40b0140bfdbf5aca189b8ddc5b22973
2024-11-28 18:03:37 +01:00
f1b59e4150 fix bug when overriding a property with bare value
the bare value must be converted to a updated property.
add also a test for this

Change-Id: I261daaaa8e12d7f739d8b2e8389c1b871b26c5b3
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34985
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
17070ca732 add sim-server again based on socketserver
- fix ls370test config file
+ fix issues with frappy_psi.ls370res
+ add frappy_psi.ls370sim

Change-Id: Ie61e3ea01c4b9c7c1286426504e50acf9413a8ba
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34957
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
d618fafe4b [deb] Release v0.20.4 2024-11-28 18:03:37 +01:00
dd1dfb3094 Lib/config: Create a list of pathes only for confdir
Under some condition (no general config file) it's possible that the
piddir and logdir as well are lists of pathes which creates some errors
during the server start

This problems occurs at least in NICOS test suite where no general
config file is defined.

Change-Id: I94c5db927923834c1546dbc34e2490b07b0bf111
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34952
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Jens Krueger <jens.krueger@tum.de>
2024-11-28 18:03:37 +01:00
8d6617e288 frappy.client: catch all errors in handleError callback
put try/execpt around handleError callback

Change-Id: I3d97f085556665189da848e52a7148248f55eb0e
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34955
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-11-28 18:03:37 +01:00
fdec531c99 PSI: Fix import error on ThermoFisher module
Change-Id: I691d8f5057fdb19ba14c109399417a7ee9962637
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34954
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-11-28 18:03:37 +01:00
a246584c4a frappy.lib.multievent: avoid deadlock
use RLock instead of Lock, as queued actions might call
the set/clear methods recursively

Change-Id: Id43aa8669955e6be9f61379d039a4f65eb7b2dc4
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34950
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
00ef174292 remove unused file
Change-Id: I969bfb22f2196227abe8c5ecef628a15e6eb75f1
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34939
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
ada66f4851 [deb] Release v0.20.3 2024-11-28 18:03:37 +01:00
a9be6475b1 add generalConfig to etc
Change-Id: I768b136c803d5e197e3653d1b84e147b62a97676
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34924
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
2024-11-28 18:03:37 +01:00
f380289a84 fixup test for cfg_editor utils to run from non-checkout, and fix names, and remove example code
Change-Id: I6224244392e2a2d0928065ba24abcbe822096084
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34934
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
2024-11-28 18:03:37 +01:00
528d80652c [deb] Release v0.20.2 2024-11-28 18:03:37 +01:00
7c6df58906 fix frappy-server cfgfiles command
frappy-server <name> errors after 34893

Change-Id: Ifba758fbabc3aef32e20b683f1c1edbfea711a75
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34913
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
2024-11-28 18:03:37 +01:00
1851c0ac43 server: better handling of cfgfile argument
No reason to keep stringly-typed data on that level

Change-Id: Iba8d88301bf36ef6051031d1916d1bac84ede546
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34893
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
2024-11-28 18:03:37 +01:00
880d472a4a generalConfig: fix the case when confdir is a list of paths
convert all env variable values containing ':' into a list of paths
+ fix one case where an env variable is not converted to a Path
+ remove unused _gcfg_help

Change-Id: Ibc51ab4606ca51e0e87d0fedfac1aca4952f3270
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34872
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-11-28 18:03:37 +01:00
25ff96873b server: service discovery over UDP.
implement RFC-005
- server broadcasts once on startup and answers to broadcasts
- small tool for listening on the port and sending broadcasts

Change-Id: I02d1184d6be62bef6f964eb9d238220aef062e94
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34851
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
2024-11-28 18:03:37 +01:00
82881049c4 systemd: enable indication of reloading/stopping
Change-Id: I6dd1b3a50234fb0304fb1a5318f2f22d35d464ec
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34896
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
2024-11-28 18:03:37 +01:00
60c9737cfe server: fix windows ctrl-c
thread.join() blocks indefinetely, not allowing python to handle the
interrupt. Same is true for sleep on windows, but when we only sleep a
second, this is fine. Instead of joining the threads, keep track of them
manually.

Change-Id: I559fe06d9ce005a15388c881e4f076d996aea9dc
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34894
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
632db924eb generalconfig: streamlined config discovery
determine generalconfig file location in order:
  - command line argument
  - environment variable
  - git location (../cfg)
  - local location (cwd)
  - global location (/etc/frappy)

Change-Id: Ie34bcbd5188837075ee7bb7d5029d676ae72378e
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34839
Reviewed-by: Bjoern Pedersen <bjoern.pedersen@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
261121297b Revert "config: allow using Prop(...)"
This reverts commit ba59bd549860797f5bdf15cadfea539754d833cd.

Reason for revert: unnecessary

Change-Id: I4bf46a1de2e699049572f376e84fa39db5dae76c
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34888
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
1bd243f3d2 config: fix typo
Change-Id: Ie90993d9b2d387780fa3faa28fd8d4523f7fc866
2024-11-28 18:03:37 +01:00
7c3f9f7196 config: allow using Prop(...)
Still maps to the same logic, but it might be a bit confusing to
configure properties with prop = Param(...)

Change-Id: I6bde6a0b015095a8b765d98cb2780f0d42de7e6e
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34886
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
2024-11-28 18:03:37 +01:00
9074dfda9d fix playground
- fix initialization
- add description

Change-Id: Ic210c26edfec709bafa902e32eae04350d571acd
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34874
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Georg Brandl <g.brandl@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
de32eb09e6 better order of accessibles: 'value' 'status' and 'target' first
- predefined parameters/commands appear first, in the order
  defined in frappy.params.PREDEFINED_ACCESSIBLES
- other (custom) parameters by inheritance order
- remove paramOrder attribute (not used currently)

Change-Id: If4c43189e4837dba057dc0a430ac6c3d1ae10829
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34904
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
2e97f0f0ce frappy_psi.sea: bugfix: revert change of updateEvent to udpateItem
revert some of change 34813
SeaClient is based on ProxyClient, not SecopClient
-> updateItem is not defined there

Change-Id: Ib3049038481917ec7a11b9fb2d285cedff5febbb
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/34873
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-11-28 18:03:37 +01:00
0b06acf304 add ori2 2024-11-26 15:16:53 +01:00
dmc
cc90291358 varioxb: fix config, om not yet available 2024-11-26 13:59:05 +01:00
03b4604643 fix description of ts in ma11stick 2024-11-15 09:14:54 +01:00
82 changed files with 5159 additions and 1346 deletions

View File

@ -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

View File

@ -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
View 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
View 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)

View File

@ -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
View 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,
)

View File

@ -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
View 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
View 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
View 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',
)

401
cfg/dil5_cfg.py Normal file
View File

@ -0,0 +1,401 @@
# by ID (independent of plug location)
turbo_uri = '/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A601PCGF-if00-port0'
press_uri = '/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AH07445U-if00-port0'
itc_uri = '/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0'
lsc_uri = '192.168.1.2:7777'
# by plug location:
#turbo_uri='/dev/serial/by-path/platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0-port0'
#press_uri = '/dev/serial/by-path/platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.2:1.0-port0'
#itc_uri = '/dev/serial/by-path/platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.3:1.0-port0'
# over USB (does not work anymore)
#lsc_uri='serial:///dev/ttyACM1?baudrate=57600+parity=odd+bytesize=7+stopbits=1',
Node('dil5.psi.ch',
'dil5 with state machine for condensing and removing',
interface='tcp://5000',
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.0",
)
Mod('V4',
'frappy_psi.logo.Valve',
'Valves',
io = 'io',
vm_address_input ="V1024.5",
vm_address_output ="V1064.7",
)
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',
target = 2000,
vm_address ="VW8",
)
Mod('pcond',
'frappy_psi.logo.safetyfeatureParam',
'Safety Feature Param',
io = 'io',
target = 1800,
vm_address ="VW10",
)
Mod('p5min',
'frappy_psi.logo.safetyfeatureParam',
'Safety Feature Param',
io = 'io',
target = 0,
vm_address ="VW12",
)
Mod('p5max',
'frappy_psi.logo.safetyfeatureParam',
'Safety Feature Param',
io = 'io',
target = 900,
vm_address ="VW14",
)
Mod('io_ls273',
'frappy_psi.ls372.StringIO',
'io for Ls372',
uri=lsc_uri,
)
Mod('sw',
'frappy_psi.ls372.Switcher',
'channel switcher',
io = 'io_ls273',
)
Mod('T_mix',
'frappy_psi.ls372.TemperatureLoop',
'mix temperature chan 5',
channel = 5,
switcher = 'sw',
)
Mod('T_ivc',
'frappy_psi.ls372.TemperatureLoop',
'mix temperature chan 2',
channel = 2,
switcher = 'sw',
)
Mod('T_still',
'frappy_psi.ls372.TemperatureLoop',
'mix temperature chan 3',
channel = 3,
switcher = 'sw',
)
Mod('T_sorb',
'frappy_psi.ls372.TemperatureLoop',
'mix temperature chan 1',
channel = 1,
switcher = 'sw',
)
Mod('T_cp',
'frappy_psi.ls372.TemperatureLoop',
'mix temperature chan 4',
channel = 4,
switcher = 'sw',
)
Mod('io_pfeiffer',
'frappy_psi.pfeiffer_new.PfeifferProtocol',
'',
uri=f'serial://{press_uri}?baudrate=9600+parity=none+bytesize=8+stopbits=1',
)
Mod('io_turbo',
'frappy_psi.pfeiffer_new.PfeifferProtocol',
'',
uri=f'serial://{turbo_uri}?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('itc',
'frappy_psi.mercury.IO',
'connection to MercuryiTC',
uri=f'serial://{itc_uri}?baudrate=115200+parity=none+bytesize=8+stopbits=1',
)
Mod('T_still_wup',
'frappy_psi.mercury.TemperatureLoop',
'still warmup temperature',
slot='MB1.T1',
io='itc',
)
Mod('T_one_K',
'frappy_psi.mercury.TemperatureLoop',
'1 K plate warmup temperature',
slot='DB5.T1',
io='itc',
)
Mod('T_mix_wup',
'frappy_psi.mercury.TemperatureLoop',
'mix. chamber warmup temperature',
slot='DB6.T1',
io='itc',
)
Mod('T_ivc_wup',
'frappy_psi.mercury.TemperatureLoop',
'IVC warmup temperature',
slot='DB7.T1',
io='itc',
)
Mod('T_cond',
'frappy_psi.mercury.TemperatureLoop',
'condenser temperature',
slot='DB8.T1',
io='itc',
)
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
View 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
View 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
View 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
View 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,
)

View File

@ -4,4 +4,4 @@ logdir = ./log
piddir = ./pid
confdir = ./cfg
comlog = True
omit_unchanged_within = 60

View File

@ -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',

21
cfg/main/haake2_cfg.py Normal file
View File

@ -0,0 +1,21 @@
Node('haake2.config.sea.psi.ch',
'Haake thermostat + Eurotherm controller',
)
Mod('sea_main',
'frappy_psi.sea.SeaClient',
'main sea connection for haakeuro.config',
config = 'haake2.config',
service = 'main',
)
Mod('th',
'frappy_psi.sea.SeaDrivable', '',
meaning = ('temperature', 10),
io = 'sea_main',
sea_object = 'th',
extra_modules=['t2'],
)
Mod('ts',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
single_module='th.t2',
)

View File

@ -1,5 +1,5 @@
Node('haakeuro.config.sea.psi.ch',
'Haake thermostat + Eurotherm controller',
Node('haake.config.sea.psi.ch',
'Haake thermostat',
)
Mod('sea_main',
'frappy_psi.sea.SeaClient',
@ -13,9 +13,5 @@ Mod('th',
io = 'sea_main',
sea_object = 'th',
extra_modules=['t2'],
)
Mod('ts',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
single_module='th.t2',
value=Param(unit='degC'),
)

View File

@ -15,6 +15,7 @@ Mod('tt',
meaning=['temperature_regulation', 27],
sea_object='tt',
rel_paths=['tt', 'set'],
value=Param(unit='K'),
)
Mod('T_ccr',
@ -22,6 +23,7 @@ Mod('T_ccr',
io='sea_main',
sea_object='tt',
rel_paths=['ccr'],
value=Param(unit='K'),
)
Mod('jtccr',
@ -101,30 +103,35 @@ Mod('p1',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='p1',
value=Param(unit='mbar'),
)
Mod('p2',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='p2',
value=Param(unit='mbar'),
)
Mod('p3',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='p3',
value=Param(unit='mbar'),
)
Mod('p4',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='p4',
value=Param(unit='mbar'),
)
Mod('pressreg',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='pressreg',
value=Param(unit='mbar'),
)
Mod('epc',

View File

@ -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',

View File

@ -7,11 +7,11 @@ Node('ori7test.psi.ch',
rack = Rack(Mod)
with rack.lakeshore() as ls:
ls.sensor('Ts', channel='C', calcurve='x186350')
ls.loop('T', channel='B', calcurve='x174786')
ls.heater('htr', '100W', 100)
rack.lakeshore()
rack.sensor('Ts', channel='C', calcurve='x186350')
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
rack.heater('htr', output_no=1, max_heater='100W', resistance=25)
rack.ccu(he=True, n2=True)
rack.hepump()
rack.he()
rack.n2()
rack.flow(min_open_pulse=0.03)

View File

@ -170,18 +170,20 @@ 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)
)
'''

View File

@ -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", "visibility": 3},
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3"},
{"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"},

View File

@ -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", "visibility": 3},
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3"},
{"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"},

View File

@ -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", "visibility": 3},
{"path": "He3", "type": "int", "readonly": false, "cmd": "dil He3"},
{"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"},

View File

@ -3,7 +3,7 @@
{"path": "unit", "type": "text", "readonly": false, "cmd": "th unit", "visibility": 3},
{"path": "t2", "type": "float"},
{"path": "set", "type": "float"},
{"path": "running", "type": "int", "readonly": false, "cmd": "th running", "visibility": 3},
{"path": "pumprunning", "type": "int", "readonly": false, "cmd": "th pumprunning"},
{"path": "extcontrol", "type": "int", "readonly": false, "cmd": "th extcontrol", "visibility": 3},
{"path": "relais", "type": "int", "visibility": 3},
{"path": "overtemp", "type": "int", "visibility": 3},

160
cfg/sea/haake2.config.json Normal file
View File

@ -0,0 +1,160 @@
{"th": {"base": "/th", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run th", "kids": 26},
{"path": "unit", "type": "text", "readonly": false, "cmd": "th unit", "visibility": 3},
{"path": "t2", "type": "float"},
{"path": "set", "type": "float"},
{"path": "pumprunning", "type": "int", "readonly": false, "cmd": "th pumprunning"},
{"path": "extcontrol", "type": "int", "readonly": false, "cmd": "th extcontrol", "visibility": 3},
{"path": "relais", "type": "int", "visibility": 3},
{"path": "overtemp", "type": "int", "visibility": 3},
{"path": "lowlevel", "type": "int", "visibility": 3},
{"path": "pumpalarm", "type": "int", "visibility": 3},
{"path": "externalarm", "type": "int", "visibility": 3},
{"path": "coolalarm", "type": "int", "visibility": 3},
{"path": "sensor1alarm", "type": "int", "visibility": 3},
{"path": "sensor2alarm", "type": "int", "visibility": 3},
{"path": "reset", "type": "int", "readonly": false, "cmd": "th reset", "visibility": 3},
{"path": "with2sensors", "type": "int", "readonly": false, "cmd": "th with2sensors", "visibility": 3},
{"path": "upperLimit", "type": "float", "readonly": false, "cmd": "th upperLimit"},
{"path": "lowerLimit", "type": "float", "readonly": false, "cmd": "th lowerLimit"},
{"path": "tolerance", "type": "float", "readonly": false, "cmd": "th tolerance"},
{"path": "maxwait", "type": "int", "readonly": false, "cmd": "th maxwait"},
{"path": "settle", "type": "int", "readonly": false, "cmd": "th settle"},
{"path": "targetValue", "type": "float"},
{"path": "is_running", "type": "int", "visibility": 3},
{"path": "verbose", "type": "int", "readonly": false, "cmd": "th verbose", "visibility": 3},
{"path": "driver", "type": "text", "visibility": 3},
{"path": "creationCmd", "type": "text", "visibility": 3},
{"path": "status", "type": "text", "readonly": false, "cmd": "th status"}]},
"te": {"base": "/te", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run te", "kids": 30},
{"path": "unit", "type": "text", "readonly": false, "cmd": "te unit", "visibility": 3},
{"path": "mode", "type": "int", "readonly": false, "cmd": "te mode"},
{"path": "model", "type": "text", "visibility": 3},
{"path": "pbPow", "type": "float", "visibility": 3},
{"path": "pbMin", "type": "float", "visibility": 3},
{"path": "pbScl", "type": "float", "visibility": 3},
{"path": "output", "type": "float"},
{"path": "position", "type": "float", "readonly": false, "cmd": "te position"},
{"path": "asymmetry", "type": "float", "readonly": false, "cmd": "te asymmetry", "visibility": 3},
{"path": "range", "type": "float", "readonly": false, "cmd": "te range", "visibility": 3},
{"path": "set", "type": "float", "readonly": false, "cmd": "te set"},
{"path": "rdonly", "type": "int", "readonly": false, "cmd": "te rdonly", "visibility": 3},
{"path": "task", "type": "text", "readonly": false, "cmd": "te task"},
{"path": "upperLimit", "type": "float", "readonly": false, "cmd": "te upperLimit"},
{"path": "lowerLimit", "type": "float", "readonly": false, "cmd": "te lowerLimit", "visibility": 3},
{"path": "tolerance", "type": "float", "readonly": false, "cmd": "te tolerance"},
{"path": "maxwait", "type": "int", "readonly": false, "cmd": "te maxwait"},
{"path": "settle", "type": "int", "readonly": false, "cmd": "te settle"},
{"path": "targetValue", "type": "float"},
{"path": "is_running", "type": "int", "visibility": 3},
{"path": "verbose", "type": "int", "readonly": false, "cmd": "te verbose", "visibility": 3},
{"path": "driver", "type": "text", "visibility": 3},
{"path": "creationCmd", "type": "text", "visibility": 3},
{"path": "status", "type": "text", "readonly": false, "cmd": "te status"},
{"path": "pb", "type": "float", "readonly": false, "cmd": "te pb"},
{"path": "ti", "type": "float", "readonly": false, "cmd": "te ti"},
{"path": "td", "type": "float", "readonly": false, "cmd": "te td"},
{"path": "manual", "type": "float", "readonly": false, "cmd": "te manual"},
{"path": "rate", "type": "float", "readonly": false, "cmd": "te rate"},
{"path": "workset", "type": "float", "readonly": false, "cmd": "te workset"}]},
"cc": {"base": "/cc", "params": [
{"path": "", "type": "bool", "kids": 96},
{"path": "send", "type": "text", "readonly": false, "cmd": "cc send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "autodevice", "type": "bool", "readonly": false, "cmd": "cc autodevice"},
{"path": "fav", "type": "bool", "readonly": false, "cmd": "cc fav"},
{"path": "f", "type": "float", "visibility": 3},
{"path": "fs", "type": "enum", "enum": {"ok": 0, "no_sens": 1}, "readonly": false, "cmd": "cc fs", "visibility": 3},
{"path": "mav", "type": "bool", "readonly": false, "cmd": "cc mav"},
{"path": "fm", "type": "enum", "enum": {"idle": 0, "opening": 1, "closing": 2, "opened": 3, "closed": 4, "no_motor": 5}, "visibility": 3},
{"path": "fa", "type": "enum", "enum": {"fixed": 0, "controlled": 1, "automatic": 2, "offline": 3}, "readonly": false, "cmd": "cc fa", "visibility": 3},
{"path": "mp", "type": "float", "readonly": false, "cmd": "cc mp", "visibility": 3},
{"path": "msp", "type": "float", "visibility": 3},
{"path": "mmp", "type": "float", "visibility": 3},
{"path": "mc", "type": "float", "readonly": false, "cmd": "cc mc", "visibility": 3},
{"path": "mfc", "type": "float", "readonly": false, "cmd": "cc mfc", "visibility": 3},
{"path": "moc", "type": "float", "readonly": false, "cmd": "cc moc", "visibility": 3},
{"path": "mtc", "type": "float", "readonly": false, "cmd": "cc mtc", "visibility": 3},
{"path": "mtl", "type": "float", "visibility": 3},
{"path": "mft", "type": "float", "readonly": false, "cmd": "cc mft", "visibility": 3},
{"path": "mt", "type": "float", "visibility": 3},
{"path": "mo", "type": "float", "visibility": 3},
{"path": "mcr", "type": "float", "visibility": 3},
{"path": "mot", "type": "float", "visibility": 3},
{"path": "mw", "type": "float", "readonly": false, "cmd": "cc mw", "description": "correction pulse after automatic open", "visibility": 3},
{"path": "hav", "type": "enum", "type": "enum", "enum": {"none": 0, "int": 1, "ext": 2}, "readonly": false, "cmd": "cc hav"},
{"path": "h", "type": "float", "visibility": 3},
{"path": "hr", "type": "float", "visibility": 3},
{"path": "hc", "type": "float", "visibility": 3},
{"path": "hu", "type": "float", "visibility": 3},
{"path": "hh", "type": "float", "readonly": false, "cmd": "cc hh", "visibility": 3},
{"path": "hl", "type": "float", "readonly": false, "cmd": "cc hl", "visibility": 3},
{"path": "htf", "type": "float", "readonly": false, "cmd": "cc htf", "description": "meas. period in fast mode", "visibility": 3},
{"path": "hts", "type": "float", "readonly": false, "cmd": "cc hts", "description": "meas. period in slow mode", "visibility": 3},
{"path": "hd", "type": "float", "readonly": false, "cmd": "cc hd", "visibility": 3},
{"path": "hwr", "type": "float", "readonly": false, "cmd": "cc hwr", "visibility": 3},
{"path": "hem", "type": "float", "readonly": false, "cmd": "cc hem", "description": "sensor length in mm from top to empty pos.", "visibility": 3},
{"path": "hfu", "type": "float", "readonly": false, "cmd": "cc hfu", "description": "sensor length in mm from top to full pos.", "visibility": 3},
{"path": "hcd", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3, "manual": 7}, "readonly": false, "cmd": "cc hcd", "visibility": 3},
{"path": "hv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4}, "visibility": 3},
{"path": "hsf", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "ha", "type": "bool", "readonly": false, "cmd": "cc ha", "visibility": 3},
{"path": "hm", "type": "bool", "visibility": 3},
{"path": "hf", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf", "visibility": 3},
{"path": "hbe", "type": "bool", "readonly": false, "cmd": "cc hbe", "visibility": 3},
{"path": "hmf", "type": "float", "visibility": 3},
{"path": "hms", "type": "float", "visibility": 3},
{"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit", "visibility": 3},
{"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft", "visibility": 3},
{"path": "hea", "type": "enum", "enum": {"0": 0, "1": 1, "6": 6}, "readonly": false, "cmd": "cc hea"},
{"path": "hch", "type": "int", "readonly": false, "cmd": "cc hch", "visibility": 3},
{"path": "hwr0", "type": "float", "readonly": false, "cmd": "cc hwr0", "visibility": 3},
{"path": "hem0", "type": "float", "readonly": false, "cmd": "cc hem0", "description": "sensor length in mm from top to empty pos.", "visibility": 3},
{"path": "hfu0", "type": "float", "readonly": false, "cmd": "cc hfu0", "description": "sensor length in mm from top to full pos.", "visibility": 3},
{"path": "hd0", "type": "float", "readonly": false, "cmd": "cc hd0", "description": "external sensor drive current (mA)", "visibility": 3},
{"path": "h0", "type": "float", "visibility": 3},
{"path": "hs0", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h1", "type": "float", "visibility": 3},
{"path": "hs1", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h2", "type": "float", "visibility": 3},
{"path": "hs2", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h3", "type": "float", "visibility": 3},
{"path": "hs3", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h4", "type": "float", "visibility": 3},
{"path": "hs4", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h5", "type": "float", "visibility": 3},
{"path": "hs5", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "hfb", "type": "float", "visibility": 3},
{"path": "nav", "type": "enum", "type": "enum", "enum": {"none": 0, "int": 1, "ext": 2}, "readonly": false, "cmd": "cc nav"},
{"path": "nu", "type": "float", "visibility": 3},
{"path": "nl", "type": "float", "visibility": 3},
{"path": "nth", "type": "float", "readonly": false, "cmd": "cc nth", "visibility": 3},
{"path": "ntc", "type": "float", "readonly": false, "cmd": "cc ntc", "visibility": 3},
{"path": "ntm", "type": "float", "readonly": false, "cmd": "cc ntm", "visibility": 3},
{"path": "ns", "type": "enum", "enum": {"sens_ok": 0, "no_sens": 1, "short_circuit": 2, "upside_down": 3, "sens_warm": 4, "empty": 5}, "visibility": 3},
{"path": "na", "type": "bool", "readonly": false, "cmd": "cc na", "visibility": 3},
{"path": "nv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4, "boost": 5}, "visibility": 3},
{"path": "nc", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3}, "readonly": false, "cmd": "cc nc", "visibility": 3},
{"path": "nfb", "type": "float", "visibility": 3},
{"path": "cda", "type": "float"},
{"path": "cdb", "type": "float"},
{"path": "cba", "type": "float"},
{"path": "cbb", "type": "float"},
{"path": "cvs", "type": "int"},
{"path": "csp", "type": "int"},
{"path": "cdv", "type": "text", "readonly": false, "cmd": "cc cdv"},
{"path": "cic", "type": "text", "readonly": false, "cmd": "cc cic"},
{"path": "cin", "type": "text"},
{"path": "cds", "type": "enum", "enum": {"local": 0, "remote": 1, "loading": 2, "by_code": 3, "by_touch": 4}, "readonly": false, "cmd": "cc cds"},
{"path": "timing", "type": "bool", "readonly": false, "cmd": "cc timing"},
{"path": "tc", "type": "float", "visibility": 3},
{"path": "tn", "type": "float", "visibility": 3},
{"path": "th", "type": "float", "visibility": 3},
{"path": "tf", "type": "float", "visibility": 3},
{"path": "tm", "type": "float", "visibility": 3},
{"path": "tv", "type": "float", "visibility": 3},
{"path": "tq", "type": "float", "visibility": 3},
{"path": "bdl", "type": "float", "readonly": false, "cmd": "cc bdl"}]}}

View File

@ -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'),
)

144
debian/changelog vendored
View File

@ -1,4 +1,48 @@
frappy-core (0.20.4) jammy; urgency=medium
frappy-core (0.20.5) stable; urgency=medium
[ Markus Zolliker ]
* add sim-server again based on socketserver
* fix bug when overriding a property with bare value
* frappy.server bug fix: server name must not be a list
* frappy.server: use server name for SecNode name
* frappy.server: remove comment about opts in SecNode/Dispatcher
* follow up change for 'better order of accessibles' (34904)
* better message when a parameter is overridden by an invalid value
* pylint: increase max number of positional arguments
* an error on a write must not send an error update
* fix bug in change 35001 (better error message)
* make UPD listener work when 'tcp://' is omitted on interface
* config: do not override equipment_id with name
* equipment_id for merged configs and routed nodes
* core: alternative approach for optional accessibles
* core: simplify test for methods names
[ Georg Brandl ]
* debian: update compat
* remove wrong <weight> from fonts on Qt6
[ Markus Zolliker ]
* config: validate value and default of parameters
* config: Mod() should return config dict
* stop poller threads on shutdown
* fix overriding Parameter with value
* improve error messages on module creation
* make sure unexported modules are initialized
* change to new visibility spec
* follow-up change to 35931: make Proxy a Module
[ Konstantin Kholostov ]
* installer: add recipe to build macOS app bundle
[ Markus Zolliker ]
* frappy.client.SecopClient: fix setParameterFromString
* frappy_psi/ls370res: various bug fixes
* client: add SecopClient.execCommandFromString
* frappy.client.interactive: improve updates while driving
-- Markus Zolliker <jenkins@frm2.tum.de> Mon, 12 May 2025 14:03:22 +0200
frappy-core (0.20.4) stable; urgency=medium
[ Georg Brandl ]
* remove unused file
@ -17,7 +61,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 +71,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 +117,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 +127,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 +172,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 +182,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 +214,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 +223,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 +249,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 +274,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 +286,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 +342,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 +460,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 +481,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 +506,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 +579,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 +591,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 +629,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 +784,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 +803,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 +814,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 +906,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 +936,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 +952,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 +970,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 +984,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 +995,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 +1058,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 +1087,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 +1099,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
View File

@ -1 +0,0 @@
11

4
debian/control vendored
View File

@ -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

View File

@ -209,16 +209,20 @@ class ProxyClient:
# caches (module, parameter) = value, timestamp, readerror (internal names!)
self.cache = Cache() # dict returning Cache.undefined for missing keys
def register_callback(self, key, *args, callimmediately=None, **kwds):
def register_callback(self, key, *args, callimmediately=True, **kwds):
"""register callback functions
- key might be either:
several callbacks might be registered within one call.
ProxyClient.CALLBACK_NAMES contains all names of valid callbacks
:param key: might be either:
1) None: general callback (all callbacks)
2) <module name>: callbacks related to a module (not called for 'unhandledMessage')
3) (<module name>, <parameter name>): callback for specified parameter (only called for 'updateEvent')
- all the following arguments are callback functions. The callback name may be
given by the keyword, or, for non-keyworded arguments it is taken from the
__name__ attribute of the function
3) (<module name>, <parameter name>): callback for specified parameter
(only called for 'updateEvent' and 'updateItem')
:param args: callback functions. the callback name is taken from the the __name__ attribute of the function
:param callimmediately: True (default): call immediately for updateItem and updateEvent callbacks
:param kwds: callback functions. the callback name is taken from the key
"""
for cbfunc in args:
kwds[cbfunc.__name__] = cbfunc
@ -226,8 +230,8 @@ class ProxyClient:
if cbname not in self.CALLBACK_NAMES:
raise TypeError(f"unknown callback: {', '.join(kwds)}")
# immediately call for some callback types
if cbname in ('updateItem', 'updateEvent') and callimmediately is not False:
# call immediately for some callback types
if cbname in ('updateItem', 'updateEvent') and callimmediately:
if key is None: # case generic callback
cbargs = [(m, p, d) for (m, p), d in self.cache.items()]
else:
@ -734,7 +738,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 +757,28 @@ class SecopClient(ProxyClient):
data = datatype.import_value(data)
return data, qualifiers
def execCommandFromString(self, module, command, formatted_argument=''):
"""call command from string argument
return data as CacheItem which allows to get
- result.value # the python value
- result.formatted() # a string (incl. units)
- result.timestamp
"""
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
# pylint: disable=unsubscriptable-object
data, qualifiers = self.request(COMMANDREQUEST, self.identifier[module, command], argument)[2]
datatype = self.modules[module]['commands'][command]['datatype'].result
value = datatype.import_value(data) if datatype else None
return CacheItem(value, qualifiers.get('t'), None, datatype)
def updateValue(self, module, param, value, timestamp, readerror):
datatype = self.modules[module]['parameters'][param]['datatype']
if readerror:

View File

@ -143,7 +143,7 @@ class Module:
def _isBusy(self):
return self.status[0] // 100 == StatusType.BUSY // 100
def _status_value_update(self, m, p, status, t, e):
def _status_update(self, m, p, status, t, e):
if self._is_driving and not self._isBusy():
self._is_driving = False
self._driving_event.set()
@ -216,10 +216,11 @@ class Module:
def __call__(self, target=None):
if target is None:
return self.read()
for pname in 'value', 'status':
watch_params = ['value', 'status']
for pname in watch_params:
self._secnode.register_callback((self._name, pname),
callimmediately=False,
updateEvent=self._watch_parameter)
updateEvent=self._watch_parameter,
callimmediately=False)
self.target = target # this sets self._is_driving
@ -239,11 +240,10 @@ class Module:
pass
clientenv.raise_with_short_traceback(e)
finally:
# self._watch_parameter(self._name, 'status')
self._secnode.readParameter(self._name, 'value')
# self._watch_parameter(self._name, 'value', forced=True)
self._secnode.unregister_callback((self._name, 'value'), updateEvent=self._watch_parameter)
self._secnode.unregister_callback((self._name, 'status'), updateEvent=self._watch_parameter)
for pname in watch_params:
self._secnode.unregister_callback((self._name, pname),
updateEvent=self._watch_parameter)
return self.value
def __repr__(self):
@ -418,8 +418,7 @@ class Client(SecopClient):
attrs[cname] = Command(cname, modname, self)
mobj = type(f'M_{modname}', (Module,), attrs)(modname, self)
if 'status' in mobj._parameters:
self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update)
self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update)
self.register_callback((modname, 'status'), updateEvent=mobj._status_update)
clientenv.namespace[modname] = mobj
if removed_modules:
self.log.info('removed modules: %s', ' '.join(removed_modules))
@ -450,7 +449,7 @@ def run(filepath):
"__file__": filepath,
"__name__": "__main__",
})
with filepath.open('rb') as file:
with open(filepath, 'rb') as file:
# pylint: disable=exec-used
exec(compile(file.read(), filepath, 'exec'), clientenv.namespace, None)

View File

@ -16,13 +16,13 @@
#
# Module authors:
# Alexander Zaft <a.zaft@fz-juelich.de>
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import os
from pathlib import Path
import re
from collections import Counter
from frappy.errors import ConfigError
from frappy.lib import generalConfig
@ -88,17 +88,50 @@ class Mod(dict):
for member in members:
self[member]['group'] = group
def override(self, **kwds):
name = self['name']
warnings = []
for key, ovr in kwds.items():
if isinstance(ovr, Group):
warnings.append(f'ignore Group when overriding module {name}')
continue
param = self.get(key)
if param is None:
self[key] = ovr if isinstance(ovr, Param) else Param(ovr)
continue
if isinstance(param, Param):
if isinstance(ovr, Param):
param.update(ovr)
else:
param['value'] = ovr
else: # description or cls
self[key] = ovr
return warnings
class Collector:
def __init__(self, cls):
self.list = []
self.cls = cls
def __init__(self):
self.modules = {}
self.warnings = []
def add(self, *args, **kwds):
self.list.append(self.cls(*args, **kwds))
mod = Mod(*args, **kwds)
name = mod.pop('name')
if name in self.modules:
self.warnings.append(f'duplicate module {name} overrides previous')
self.modules[name] = mod
return mod
def append(self, mod):
self.list.append(mod)
def override(self, name, **kwds):
"""override properties/parameters of previously configured modules
this is useful together with 'include'
"""
mod = self.modules.get(name)
if mod is None:
self.warnings.append(f'try to override nonexisting module {name}')
return
self.warnings.extend(mod.override(**kwds))
class NodeCollector:
@ -111,14 +144,16 @@ class NodeCollector:
else:
raise ConfigError('Only one Node is allowed per file!')
def override(self, **kwds):
if self.node is None:
raise ConfigError('node must be defined before overriding')
self.node.update(kwds)
class Config(dict):
def __init__(self, node, modules):
super().__init__(
node=node.node,
**{mod['name']: mod for mod in modules.list}
)
self.module_names = {mod.pop('name') for mod in modules.list}
super().__init__(node=node.node, **modules.modules)
self.module_names = set(modules.modules)
self.ambiguous = set()
def merge_modules(self, other):
@ -134,25 +169,35 @@ class Config(dict):
mod['original_id'] = equipment_id
class Include:
def __init__(self, namespace, log):
self.namespace = namespace
self.log = log
def __call__(self, cfgfile):
filename = to_config_path(cfgfile, self.log, '')
# pylint: disable=exec-used
exec(compile(filename.read_bytes(), filename, 'exec'), self.namespace)
def process_file(filename, log):
config_text = filename.read_bytes()
node = NodeCollector()
mods = Collector(Mod)
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group}
mods = Collector()
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group,
'override': mods.override, 'overrideNode': node.override}
ns['include'] = Include(ns, log)
# pylint: disable=exec-used
exec(compile(config_text, filename, 'exec'), ns)
# check for duplicates in the file itself. Between files comes later
duplicates = [name for name, count in Counter([mod['name']
for mod in mods.list]).items() if count > 1]
if duplicates:
log.warning('Duplicate module name in file \'%s\': %s',
filename, ','.join(duplicates))
if mods.warnings:
log.warning('warnings in %s', filename)
for text in mods.warnings:
log.warning(text)
return Config(node, mods)
def to_config_path(cfgfile, log):
def to_config_path(cfgfile, log, check_end='_cfg.py'):
candidates = [cfgfile + e for e in ['_cfg.py', '.py', '']]
if os.sep in cfgfile: # specified as full path
file = Path(cfgfile) if Path(cfgfile).exists() else None
@ -166,8 +211,8 @@ def to_config_path(cfgfile, log):
file = None
if file is None:
raise ConfigError(f"Couldn't find cfg file {cfgfile!r} in {generalConfig.confdir}")
if not file.name.endswith('_cfg.py'):
log.warning("Config files should end in '_cfg.py': %s", file.name)
if not file.name.endswith(check_end):
log.warning("Config files should end in %r: %s", check_end, file.name)
log.debug('Using config file %s for %s', file, cfgfile)
return file
@ -195,6 +240,8 @@ def load_config(cfgfiles, log):
config.merge_modules(cfg)
else:
config = cfg
if config.get('node') is None:
raise ConfigError(f'missing Node in {filename}')
if config.ambiguous:
log.warning('ambiguous sections in %s: %r',

View File

@ -27,7 +27,6 @@
<property name="font">
<font>
<pointsize>18</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>

View File

@ -21,7 +21,6 @@
<property name="font">
<font>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
<underline>true</underline>
</font>

View File

@ -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}'")
@ -522,9 +526,7 @@ class Module(HasAccessibles):
if validate:
value = pobj.datatype(value)
except Exception as e:
# allow to assign an exception to trigger an error_update message
err = value if isinstance(value, Exception) else e
changed = False
err = e
else:
changed = pobj.value != value or pobj.readerror
# store the value even in case of error
@ -611,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):
@ -624,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.
@ -657,13 +679,10 @@ class Module(HasAccessibles):
self.pollInfo.pending_errors.discard(rfunc.__name__)
except Exception as e:
if getattr(e, 'report_error', True):
self.log.debug('error in %r', rfunc)
name = rfunc.__name__
self.pollInfo.pending_errors.add(name) # trigger o.k. message after error is resolved
if isinstance(e, SECoPError):
if name == 'doPoll':
# otherwise the method is already appended in rfunc
e.raising_methods.append(f'{self.name}.{name}')
e.raising_methods.append(name)
if e.silent:
self.log.debug('%s', e.format(False))
else:
@ -671,7 +690,7 @@ class Module(HasAccessibles):
if raise_com_failed and isinstance(e, CommunicationFailedError):
raise
else:
# not a SECoPError: this is probably a programming error
# not a SECoPError: this is proabably a programming error
# we want to log the traceback
self.log.error('%s', formatException())
@ -731,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)
@ -746,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:
@ -766,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:

View File

@ -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):

View File

@ -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
@ -306,7 +316,7 @@ class Parameter(Accessible):
if modobj:
if self.update_unchanged == -1:
t = modobj.omit_unchanged_within
self.omit_unchanged_within = generalConfig.omit_unchanged_within if t is None else t
self.omit_unchanged_within = float(generalConfig.omit_unchanged_within) if t is None else t
else:
self.omit_unchanged_within = float(self.update_unchanged)
@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -142,4 +142,5 @@ class SimDrivable(SimReadable, Drivable):
@Command
def stop(self):
"""set target to value"""
self.target = self.value

View File

@ -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')):

View File

@ -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

View File

@ -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
"""

View File

@ -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"""

View File

@ -47,6 +47,28 @@ 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:
@ -58,12 +80,8 @@ class Adq:
bw_cutoff = 10E6
trigger = EXT_TRIG_1
adq_num = 1
UNDEFINED = -1
IDLE = 0
BUSY = 1
READY = 2
status = UNDEFINED
data = None
busy = False
def __init__(self):
global ADQAPI
@ -84,23 +102,24 @@ class Adq:
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)
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))
print('\nConnected to ADQ #1')
# Print revision information
print('FPGA Revision: {}'.format(revision[0]))
out = [f'Connected to ADQ #1, FPGA Revision: {revision[0]}']
if revision[1]:
print('Local copy')
out.append('Local copy')
else:
print('SVN Managed')
if revision[2]:
print('Mixed Revision')
out.append('SVN Managed - Mixed Revision')
else:
print('SVN Updated')
print('')
out.append('SVN Updated')
print(', '.join(out))
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF)
##########################
@ -126,14 +145,13 @@ class Adq:
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))
# 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"""
"""initialize dimensions and store result object"""
if samples_per_record:
self.samples_per_record = samples_per_record
if number_of_records:
@ -145,13 +163,18 @@ class Adq:
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(self.adq_cu, self.adq_num)
ADQAPI.ADQ_MultiRecordClose(self.adq_cu, self.adq_num)
ADQAPI.ADQ_DisarmTrigger(cu, self.adq_num)
ADQAPI.ADQ_MultiRecordClose(cu, self.adq_num)
# Delete ADQControlunit
ADQAPI.DeleteADQControlUnit(self.adq_cu)
ADQAPI.DeleteADQControlUnit(cu)
print('ADQ closed')
def start(self):
def start(self, data):
# Start acquisition
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
self.number_of_records,
@ -159,35 +182,36 @@ class Adq:
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
self.status = self.BUSY
self.data = data
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):
"""get new data if available"""
ready = False
data = self.data
if not data:
self.busy = False
return None # no new data
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
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:
@ -195,12 +219,20 @@ class PEdata:
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)
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2**14)) # 14 bits ADC
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
@ -253,61 +285,113 @@ class PEdata:
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()))
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)
# times.append(('filtro', time.time()))
self.timer('aviq')
iqf = self.filtro(iq, bw_cutoff)
m = len(iqf[0]) // self.ndecimate
self.timer('filtro')
m = max(1, len(iqf[0]) // self.ndecimate)
ll = m * self.ndecimate
iqf = [iqfx[0:ll] for iqfx in iqf]
# times.append(('iqdec', time.time()))
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])
# times.append(('pulsig', time.time()))
self.timer('pulsig')
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]
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):
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
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
self.inp = Namespace(idx=0, name='input')
self.out = Namespace(idx=1, name='output')
self.channels = (self.inp, self.out)
self.timer = Timer()
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 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')
def calc_quality(self):
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_std and output_std:
- input_stddev:
the imaginary part indicates deviations in phase
the real part indicates deviations in amplitude
- input_slope and output_slope:
- 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()
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
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]
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
View 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)

View File

@ -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):

View 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()

View File

@ -1,125 +1,144 @@
import os
from glob import glob
from pathlib import Path
from configparser import ConfigParser
class Lsc:
def __init__(self, modfactory, ls_uri, ls_ioname='lsio', ls_devname='ls', ls_model='336', **kwds):
self.modfactory = Mod = modfactory
self.model = ls_model
self.ioname = ls_ioname
self.devname = ls_devname
self.io = Mod(self.ioname, cls=f'frappy_psi.lakeshore.IO{self.model}',
description='comm. to lakeshore in cc rack',
uri=ls_uri)
self.dev = Mod(self.devname, cls=f'frappy_psi.lakeshore.Device{self.model}',
description='lakeshore in cc rack', io=self.ioname, curve_handling=True)
self.loops = {}
self.outputs = {}
def sensor(self, name, channel, calcurve, **kwds):
Mod = self.modfactory
kwds.setdefault('cls', f'frappy_psi.lakeshore.Sensor{self.model}')
kwds.setdefault('description', f'T sensor {name}')
return Mod(name, channel=channel, calcurve=calcurve,
io=self.ioname, device=self.devname, **kwds)
def loop(self, name, channel, calcurve, **kwds):
Mod = self.modfactory
kwds.setdefault('cls', f'frappy_psi.lakeshore.Loop{self.model}')
kwds.setdefault('description', f'T loop {name}')
mod = Mod(name, channel=channel, calcurve=calcurve,
io=self.ioname, device=self.devname, **kwds)
self.loops[name] = mod
return mod
def heater(self, name, max_heater, resistance, output_no=1, **kwds):
Mod = self.modfactory
if output_no == 1:
kwds.setdefault('cls', f'frappy_psi.lakeshore.MainOutput{self.model}')
elif output_no == 2:
kwds.setdefault('cls', f'frappy_psi.lakeshore.SecondaryOutput{self.model}')
else:
return
kwds.setdefault('description', '')
mod = Mod(name, max_heater=max_heater, resistance=resistance,
io=self.ioname, device=self.devname, **kwds)
self.outputs[name] = mod
return mod
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
outmodules = dict(self.outputs)
for name, loop in self.loops.items():
outname = loop.get('output_module')
if outname:
out = outmodules.pop(outname, None)
if not out:
raise KeyError(f'{outname} is not a output module in this lakeshore')
else:
if not outmodules:
raise KeyError(f'{name} needs an output module on this lakeshore')
outname = list(outmodules)[0]
out = outmodules.pop(outname)
loop['output_module'] = outname
if not out['description']:
out['description'] = f'heater for {outname}'
class CCU:
def __init__(self, Mod, ccu_uri, ccu_ioname='ccuio', ccu_devname='ccu', he=None, n2=None, **kwds):
self.ioname = ccu_ioname
self.devname = ccu_devname
Mod(self.ioname, 'frappy_psi.ccu4.CCU4IO',
'comm. to CCU4', uri=ccu_uri)
if he:
if not isinstance(he, str): # e.g. True
he = 'He_lev'
Mod(he, cls='frappy_psi.ccu4.HeLevel',
description='the He Level', io=self.ioname)
if n2:
if isinstance(n2, str):
n2 = n2.split(',')
else: # e.g. True
n2 = []
n2, valve, upper, lower = n2 + ['N2_lev', 'N2_valve', 'N2_upper', 'N2_lower'][len(n2):]
print(n2, valve, upper, lower)
Mod(n2, cls='frappy_psi.ccu4.N2Level',
description='the N2 Level', io=self.ioname,
valve=valve, upper=upper, lower=lower)
Mod(valve, cls='frappy_psi.ccu4.N2FillValve',
description='LN2 fill valve', io=self.ioname)
Mod(upper, cls='frappy_psi.ccu4.N2TempSensor',
description='upper LN2 sensor')
Mod(lower, cls='frappy_psi.ccu4.N2TempSensor',
description='lower LN2 sensor')
class HePump:
def __init__(self, Mod, hepump_uri, hepump_io='hepump_io', hemotname='hepump_mot', **kwds):
Mod(hepump_io, 'frappy_psi.trinamic.BytesIO', 'He pump connection', uri=hepump_uri)
Mod(hemotname, 'frappy_psi.trinamic.Motor', 'He pump valve motor', io=hepump_io)
from frappy.errors import ConfigError
class Rack:
rackfile = '/home/l_samenv/.config/racks.ini'
configbase = Path('/home/l_samenv/.config/frappy_instruments')
def __init__(self, modfactory, **kwds):
self.modfactory = modfactory
parser = ConfigParser()
parser.optionxform = str
parser.read([self.rackfile])
kwds.update(parser.items(os.environ['Instrument']))
self.kwds = kwds
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 lakeshore(self):
return Lsc(self.modfactory, **self.kwds)
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 ccu(self, **kwds):
kwds.update(self.kwds)
return CCU(self.modfactory, **kwds)
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 hepump(self):
return HePump(self.modfactory, **self.kwds)
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)

View File

@ -22,21 +22,24 @@
"""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
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'
@ -44,8 +47,8 @@ class CCU4IO(StringIO):
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
@ -79,7 +82,7 @@ class CCU4Base(HasIO):
return result
class HeLevel(CCU4Base, Readable):
class HeLevel(Base, Readable):
"""He Level channel of CCU4"""
value = Parameter(unit='%')
@ -121,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')),
@ -173,7 +176,7 @@ class N2TempSensor(Readable):
value = Parameter('LN2 T sensor', FloatRange(unit='K'), default=0)
class N2Level(CCU4Base, Readable):
class N2Level(Base, Readable):
valve = Attached(Writable, mandatory=False)
lower = Attached(Readable, mandatory=False)
upper = Attached(Readable, mandatory=False)
@ -183,9 +186,9 @@ class N2Level(CCU4Base, Readable):
mode = Parameter('auto mode', EnumType(A), readonly=False, default=A.manual)
threshold = Parameter('threshold triggering start/stop filling',
FloatRange(unit='K'), readonly=False)
FloatRange(unit='K'), readonly=False, default=90)
cool_delay = Parameter('max. minutes needed to cool the lower sensor',
FloatRange(unit='s'), readonly=False)
FloatRange(unit='s'), readonly=False, default=30)
fill_timeout = Parameter('max. minutes needed to fill',
FloatRange(unit='s'), readonly=False)
names = Property('''names of attached modules
@ -285,189 +288,515 @@ class N2Level(CCU4Base, Readable):
self.command(nc=0)
class FlowPressure(CCU4Base, Readable):
class N2LevelGuess(N2Level):
"""guess the current level from hold time"""
value = Parameter('estimated level', FloatRange(unit='%'), default=20)
fill_time = Parameter('min fill time - for raw level indicator',
FloatRange(unit='s'), default=600)
hold_time = Parameter('min hold time - for raw level indicator',
FloatRange(unit='s'), default=24 * 3600)
_full_since = None
_empty_since = None
_fill_state = '' # may also be 'empty', 'full' or 'unknown'
_lower = 0
_upper = 0
def read_status(self):
status = super().read_status()
if status == (IDLE, ''):
return IDLE, self._fill_state
return status
def read_value(self):
# read sensors
now = time.time()
lower, upper = self.command(nl=float, nu=float)
if self.lower:
self.lower.value = lower
if self.upper:
self.upper.value = upper
if upper < self.threshold:
self._full_since = now
if self._empty_since is not None:
self.fill_time = now - self._empty_since
self._empty_since = None
self._fill_state = 'full'
return 100
if lower < self.threshold:
if self._empty_since is None:
if self._full_since is None:
self._fill_state = 'unknown'
return 20
delay = now - self._full_since
value = max(10, 100 * 1 - delay / self.hold_time)
if value < 99:
self._fill_state = ''
return value
delay = now - self._empty_since - self.cool_delay
value = min(90, 100 * max(0, delay / self.fill_time))
if value >= 10:
self._fill_state = ''
return value
if self._full_since is not None:
self.hold_time = now - self._full_since
self._full_since = None
self.log.info('lower %g upper %g threshold %g', lower, upper, self.threshold)
self._empty_since = now
self._fill_state = 'empty'
return 0
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)
pump_type = Parameter('pump type', EnumType(unknown=0, neodry=1, xds35=2, sv65=3),
readonly=False, value=0)
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)
flow_closed = Parameter('flow when needle valve is closed', FloatRange(unit='ln/min'),
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)
FLOW_SCALE = {'unknown': 1, 'neodry': 0.55, 'xds35': 0.6, 'sv65': 0.9}
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.debug('change target')
self.start_machine(self.change_target, target=value, try_close=True)
def write_pump_type(self, value):
self.pressure_scale = self.FLOW_SCALE[value.name]
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):
self.target = sm.target
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.debug('target %s value %s', self.target, self._value)
tol = self._tolerance(self.target)
if abs(self.target - self._value) < tol:
self.log.debug('go to at_target')
return self.at_target
self.log.debug('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 _dif_medians(self):
return np.array([self.target - np.median(self._last[-m:]) for m in (1, 5, 12, 30, 60)])
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 = self._dif_medians()
if sm.init:
self.log.debug('restart controlling')
direction = math.copysign(1, dif[1])
if direction != self._dir:
self.log.debug('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.debug('overshoot %r', dif)
return self.controlling
if not np.any(difdir < -tol):
# within tolerance
self.log.debug('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:
lim = self.flow_closed + 0.5
if sm.try_close and self._value <= lim - tol and self.target >= lim + tol:
sm.try_close = False
self.command(mp=-60)
sm.after_close = self.open_until_flow_increase
self.log.debug('go to closing / open_until_flow_increase')
return self.closing
if sm.no_progress_pulse:
pulse = abs(sm.no_progress_pulse[self._dir < 0]) * self._dir
self.log.debug('not enough progress %g %r', pulse, sm.try_close)
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.debug('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 = self._dif_medians()
if np.all(dif > tol) or np.all(dif < -tol):
self.log.debug('unstable %r %g', 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, after_close=None, 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:
if sm.after_close:
return sm.after_close
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 close_test(self):
"""close and then try to open until the flow starts to increase
save a
"""
self.command(mp=-60)
self.start_machine(self.closing, fast_poll=0.1, after_close=self.open_until_flow_increase, target=0)
@status_code(BUSY)
def open_until_flow_increase(self, sm):
if sm.init:
p = self.command(f=float)
sm.threshold = 0.5
sm.prev = [p]
sm.ref = p
sm.cnt = 0
sm.low_flow = 0
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.flow_closed = sm.low_flow
self.log.debug('flow increased %g', press)
if sm.target == 0:
sm.target = sm.low_flow + 0.5
return self.change_target
self.log.debug('wait count %g', press)
return Retry
sm.low_flow = self.value
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.debug('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.debug('measured %g new threshold %g press %g', measured, sm.threshold, press)
else:
self._speed_sum += 1
self.log.debug('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.debug('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.debug('before %g pulse %g, flowstep %g', sm.flow_before, sm.open_pulse, sm.last[-1] - sm.flow_before)
self.close()
return self.final_status(IDLE, '')

View File

@ -0,0 +1,320 @@
# *****************************************************************************
#
# 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('Higher limit for condenseline pressure', IntRange())
target = Parameter('target state', EnumType(Targetstates))
value = Parameter('current state', EnumType(Targetstates))
init = True
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)
def wait_for_condense_line_pressure(self, state):
if (self.condenseline_pressure.read_value > 500):
return Retry
self.condense_valve.write_target(0)
return self.circulate
def initialize_condense_valves(self, state):
raise NotImplementedError
@status_code(BUSY)
def condense(self, state):
"""Führt das Kondensationsverfahren durch."""
if state.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)
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):
return self.condense_wait_before_turbo_start
return self.wait_for_condense_line_pressure
@status_code(BUSY, 'condense (wait before starting turbo)')
def condense_wait_before_turbo_start(self, state):
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, state):
raise NotImplementedError
@status_code(BUSY)
def circulate(self, state):
"""Zirkuliert die Mischung."""
if state.init:
self.initialize_circulation_valves()
return Retry
@status_code(BUSY, 'remove (wait for turbo shut down)')
def remove(self, state):
"""Entfernt die Mischung."""
if state.init:
self.condenseline_valve.write_target(0)
self.dump_valve.write_target(1)
self.start_time = self.now
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:
return self.remove_wait_for_still_pressure
return remove_endsequence
@status_code(BUSY, 'remove (wait for still pressure low)')
def remove_wait_for_still_pressure(self, state):
if self.still_pressure.read_value() > 20:
return Retry
self.turbopump.write_target(1)
return self.remove_endsequence
@status_code(BUSY)
def remove_endsequence(self, state):
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)
self.remove_check_manual_valves()
self.remove_close_valves()
self.circulate_pump.write_target(0)
return Finish
def remove_check_manual_valves(self):
raise NotImplementedError
def remove_close_valves(self):
raise NotImplementedError
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]
self.remove_check_closed_valves = [self.MV11, self.MV9, self.MV12, self.MV1]
self.remove_closed_valves = [self.condenseline_valve, self.circuitshort_valve, self.V5, self.condense_valve, self.dump_valve]
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')
def remove_check_manual_valves(self):
for valve in self.remove_check_closed_valves:
if (valve.read_value() == 1):
self.final_status(WARN, "manual valve {valve.name} must be closed")
def remove_close_valves(self):
for valve in self.remove_closed_valves:
valve.write_target(0)

View File

@ -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:

View File

@ -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)
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)
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)
_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')

65
frappy_psi/hepump.py Normal file
View File

@ -0,0 +1,65 @@
# *****************************************************************************
#
# 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)
valve = Attached(Writable)
value = Parameter(datatype=BoolType())
target = Parameter(datatype=BoolType())
pump_type = NeedleValveFlow.pump_type
eco_mode = Parameter('eco mode', BoolType(), readonly=False)
has_feedback = Parameter('feedback works', BoolType(), readonly=False, default=True)
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 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

View File

@ -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)

View File

@ -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')
@ -97,5 +153,6 @@ class Plot:
self.first = False
plt.draw()
# TODO: do not know why this is needed:
self.fig.canvas.draw()
self.fig.canvas.flush_events()

View File

@ -39,6 +39,13 @@ from frappy.extparams import StructParam
from frappy_psi.calcurve import CalCurve
def string_to_num(string):
try:
return int(string)
except ValueError:
return float(string)
class IO(StringIO):
"""IO classes of most LakeShore models will inherit from this"""
end_of_line = '\n'
@ -97,8 +104,9 @@ class HasLscIO(HasIO):
if not args:
self.communicate(f'{msghead};*OPC?')
return None
converters = [type(a) for a in args]
values = [a if issubclass(c, str) else f'{a:g}'
# when an argument is given as integer, it might be that this argument might be a float
converters = [string_to_num if isinstance(a, int) else type(a) for a in args]
values = [a if isinstance(a, str) else f'{a:g}'
for c, a in zip(converters, args)]
if ' ' in msghead:
query = msghead.replace(' ', '? ', 1)
@ -839,7 +847,7 @@ class MainOutput(Output):
}
_htr_range = 1
heater_ranges = {5 - i: 10 ** - i for i in range(5)}
heater_ranges = {5 - i: 10 ** -i for i in range(5)}
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
def read_status(self):
@ -847,6 +855,9 @@ class MainOutput(Output):
return self.HTRST_MAP[st]
def configure(self):
if self._desired_max_power is None:
self.log.info(f'max_heater {self.writeDict} {self.max_heater}')
self.write_max_heater(self.max_heater)
self._htr_range = irng = self.get_best_power_idx(self._desired_max_power)
user_current = max(0.1, min(self.imax, 2 * math.sqrt(self._desired_max_power /
self.heater_ranges[irng] / self.resistance)))
@ -1180,7 +1191,7 @@ class MainOutput336(MainOutput):
imax = 2
vmax = 50
# 3 ranges only
heater_ranges = {i: 10 ** (3 - i) for i in range(5)}
heater_ranges = {3 - i: 10 ** -i for i in range(3)}
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())

268
frappy_psi/logo.py Normal file
View File

@ -0,0 +1,268 @@
# *****************************************************************************
#
# 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.comLog('> %s %s', cmd[0], cmd[1])
self._plc.write(cmd[0], literal_eval(cmd[1]))
self.comLog('< OK')
try:
self.comLog('> %s', cmd[0])
reply = self._plc.read(cmd[0])
self.comLog('< %s', reply)
return str(reply)
except Exception as e:
if self._plc:
self.comLog('? %r', e)
self.log.exception('error in plc read')
self._plc = None
raise
class Snap7Mixin(HasIO):
ioclass = IO
def get_vm_value(self, vm_address):
return literal_eval(self.io.communicate(vm_address))
def set_vm_value(self, vm_address, value):
return literal_eval(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.5)
def read_status(self):
self.log.debug('read_status')
value = self.read_value()
self.log.debug('value %d target %d', value, self.target)
if value != self.target:
if self._remaining_tries is None:
self.target = self.read_value()
return IDLE,''
self._remaining_tries -= 1
if self._remaining_tries < 0:
self.setFastPoll(False)
return ERROR, 'too many tries to switch'
self.set_vm_value(self.vm_address_output, self.target)
return BUSY, 'switching (try again)'
self.setFastPoll(False)
return IDLE, ''
class FluidMachines(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, ''

View File

@ -68,8 +68,8 @@ class LakeShoreIO(HasIO):
args.insert(0, cmd)
else:
args[0] = cmd + args[0]
head = ','.join(args)
tail = cmd.replace(' ', '?')
tail = ','.join(args)
head = cmd.replace(' ', '?')
reply = self.io.communicate(f'{head};{tail}')
return parse_result(reply)
@ -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):

View 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, ''

View File

@ -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)

View File

@ -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, ''

177
frappy_psi/pfeiffer_new.py Normal file
View File

@ -0,0 +1,177 @@
#!/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'
@staticmethod
def calculate_crc(data):
crc = sum(ord(ch) for ch in data) % 256
return f'{crc:03d}'
@staticmethod
def check_crc(data):
if data[-3:] != PfeifferProtocol.calculate_crc(data[:-3]):
raise CommunicationFailedError('Bad crc')
def communicate(self, cmd):
cmd += self.calculate_crc(cmd)
reply = super().communicate(cmd)
self.check_crc(reply)
return reply[:-3]
class PfeifferMixin(HasIO):
ioClass = PfeifferProtocol
address= Property('Addresse', datatype= IntRange())
def data_request_u_expo_new(self, parameter_nr):
reply = self.communicate(f'{self.address:03d}00{parameter_nr:03d}02=?')
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):
reply = self.communicate(f'{self.address:03d}00{parameter_nr:03d}02=?')
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):
reply = self.communicate(f'{self.address:03d}00{parameter_nr:03d}02=?')
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):
reply = self.communicate(f'{self.address:03d}00{parameter_nr:03d}02=?')
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):
reply = self.communicate(f'{self.address:03d}00{parameter_nr:03d}02=?')
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}'
reply = self.communicate(cmd)
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):
if self.target:
return BUSY, 'ramping up'
else:
return IDLE, ''
else:
return IDLE, 'at targetspeed'

View File

@ -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, 'clear_errors needed after ' + self._blocking_error
self.status = ERROR, '<motor>.clear_errors() needed after ' + self._blocking_error
raise HardwareError(self.status[1])
self.saveParameters()
self.start_machine(self.starting, target=value)

View File

@ -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
View 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)

View File

@ -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,9 +56,20 @@ 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):

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,53 @@
# -*- mode: python -*-
import sys
from os import path
sys.path.insert(0, path.abspath('.'))
from utils import find_modules, find_uis, rootdir
binscript = path.join(rootdir, 'bin', 'frappy-gui')
a = Analysis(
[binscript],
pathex=[rootdir],
binaries=[],
datas=find_uis() +
[(path.join(rootdir, 'frappy', 'RELEASE-VERSION'), 'frappy')],
hiddenimports=find_modules('frappy', 'gui'),
hookspath=[],
excludes=['matplotlib'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None
)
pyz = PYZ(
a.pure,
a.zipped_data,
cipher=None
)
exe = EXE(
pyz,
a.scripts,
exclude_binaries=True,
name='frappy-gui',
strip=False,
debug=False,
upx=False,
console=False
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
name='frappy-gui',
)
app = BUNDLE(
coll,
name='frappy-gui.app',
bundle_identifier=None,
)

View File

@ -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
View 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

View File

@ -92,8 +92,8 @@ def test_cfg_not_existing(direc, log):
def collector_helper(node, mods):
n = NodeCollector()
n.add(*node)
m = Collector(Mod)
m.list = [Mod(module, '', '') for module in mods]
m = Collector()
m.modules = {module: Mod(module, '', '') for module in mods}
return n, m

View File

@ -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)

View File

@ -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()