Compare commits

..

57 Commits
wip ... sinq

Author SHA1 Message Date
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
l_samenv
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
l_samenv
745cc69e9e sea cfg: set visibility of calibration points to expert 2025-02-13 09:39:11 +01:00
l_samenv
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
l_samenv
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
Jenkins system
d618fafe4b [deb] Release v0.20.4 2024-11-28 18:03:37 +01:00
Jens Krüger
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
Jens Krüger
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
Georg Brandl
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
Jenkins system
ada66f4851 [deb] Release v0.20.3 2024-11-28 18:03:37 +01:00
Alexander Zaft
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
Georg Brandl
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
Jenkins system
528d80652c [deb] Release v0.20.2 2024-11-28 18:03:37 +01:00
Alexander Zaft
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
Georg Brandl
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
Alexander Zaft
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
Georg Brandl
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
Alexander Zaft
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
Alexander Zaft
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
Alexander Zaft
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
Alexander Zaft
1bd243f3d2 config: fix typo
Change-Id: Ie90993d9b2d387780fa3faa28fd8d4523f7fc866
2024-11-28 18:03:37 +01:00
Alexander Zaft
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
46 changed files with 1230 additions and 3828 deletions

View File

@ -69,7 +69,7 @@ def main(argv=None):
console.setLevel(loglevel)
logger.addHandler(console)
app = QApplication(argv, organizationName='frappy', applicationName='frappy_gui')
app = QApplication(argv)
win = MainWindow(args, logger)
app.aboutToQuit.connect(win._onQuit)

View File

@ -59,23 +59,16 @@ 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} {address}:{answer.port}')
print(f'{answer.equipment_id} {answer.address}:{answer.port}')
return
print(f'Found {answer.equipment_id} at {address}{numeric}:')
print(f'Found {answer.equipment_id} at {answer.address}:')
print(f' Port: {answer.port}')
print(f' Firmware: {answer.firmware}')
desc = answer.description.replace('\n', '\n ')
print(f' Node description: {desc}')
print('-' * 80)
print()
def scan(max_wait=1.0):
@ -126,14 +119,10 @@ def listen(*, short=False):
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--listen', action='store_true',
help='Keep listening after the broadcast.')
parser.add_argument('-s', '--short', action='store_true',
help='Print short info (always on when listen).')
help='Print short info. '
'Keep listening after the broadcast.')
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=short)
print_answer(answer, short=args.listen)
if args.listen:
listen(short=short)
listen(short=args.listen)

View File

@ -1,50 +0,0 @@
#!/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')
if len(sys.argv) > 1:
maxy = float(sys.argv[1])
else:
maxy = 0.02
iqplot = Plot(maxy)
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()

View File

@ -1,65 +0,0 @@
#!/usr/bin/env python3
import sys
from pathlib import Path
# Add import path for inplace usage
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))
from frappy.client.interactive import Client
import numpy as np
import matplotlib.pyplot as plt
from frappy_psi.iqplot import Pause
if len(sys.argv) < 2:
print("""
Usage:
us-plot <end> [<start> [<npoints>]]
end: end of window [ns]
start: start of window [n2], default: 0
npoints: number fo points (default 1000)
""")
sys.exit(0)
Client('pc13252:5000')
def plot(array, ax, style, xs):
xaxis = np.arange(len(array)) * xs
return ax.plot(xaxis, array, style)[0]
def update(array, line, xs):
xaxis = np.arange(len(array)) * xs
line.set_data(np.array([xaxis, array]))
def on_close(event):
sys.exit(0)
start = 0
end = float(sys.argv[1])
npoints = 1000
if len(sys.argv) > 2:
start = float(sys.argv[2])
if len(sys.argv) > 3:
npoints = float(sys.argv[3])
fig, ax = plt.subplots(figsize=(15,3))
pause = Pause(fig)
try:
get_signal = iq.get_signal
print('plotting RUS signal')
except NameError:
get_signal = u.get_signal
print('plotting PE signal')
xs, signal = get_signal(start, end, npoints)
lines = [plot(s, ax, '-', xs) for s in signal]
while pause(0.5):
plt.draw()
xs, signal = get_signal(start, end, npoints)
for line, sig in zip(lines, signal):
update(sig, line, xs)

67
cfg/PEUS.py Normal file
View File

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

View File

@ -1,87 +0,0 @@
Node('PEUS.psi.ch',
'ultrasound, pulse_echo configuration',
interface='5000',
)
Mod('u',
'frappy_psi.ultrasound.PulseEcho',
'ultrasound acquisition loop',
freq='f',
# pollinterval=0.1,
time=900.0,
size=5000.0,
nr=500,
sr=32768,
bw=1e7,
)
Mod('fio',
'frappy_psi.ultrasound.FreqStringIO', '',
uri='serial:///dev/ttyS1?baudrate=57600',
)
Mod('f',
'frappy_psi.ultrasound.Frequency',
'writable for frequency',
output='R', # L for LF (bnc), R for RF (type N)
io='fio',
amp=0.5, # VPP
)
Mod('fdif',
'frappy_psi.ultrasound.FrequencyDif',
'writable for frequency minus base frequency',
freq='f',
base=41490200.0,
)
# Mod('curves',
# 'frappy_psi.ultrasound.Curves',
# 't, I, Q and pulse arrays for plot',
# )
def roi(name, time, size, components='iqpa', enable=True, control=False, freq=None, **kwds):
description = 'I/Q of region {name}'
if freq:
kwds.update(cls='frappy_psi.ultrasound.ControlRoi',
description=f'{description} as control loop',
freq=freq, **kwds)
else:
kwds.update(cls='frappy_psi.ultrasound.Roi',
description=description, **kwds)
kwds.update({c: name + c for c in components})
Mod(name,
main='u',
time=time,
size=size,
enable=enable,
**kwds,
)
for c in components:
Mod(name + c,
'frappy.modules.Readable',
f'{name}{c} component',
)
# control loop
roi('roi0', 2450, 300, freq='f', maxstep=100000, minstep=4000)
# other rois
roi('roi1', 5950, 300)
roi('roi2', 9475, 300)
roi('roi3', 12900, 300)
#roi('roi4', 400, 30, False)
#roi('roi5', 400, 30, False)
#roi('roi6', 400, 30, False)
#roi('roi7', 400, 30, False)
#roi('roi8', 400, 30, False)
#roi('roi9', 400, 30, False)
Mod('delay',
'frappy_psi.dg645.Delay',
'delay line with 2 channels',
uri='serial:///dev/ttyS2',
on1=1e-09,
on2=1e-09,
off1=4e-07,
off2=6e-07,
)

62
cfg/RUS.py Normal file
View File

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

View File

@ -1,39 +0,0 @@
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 Executable file → Normal file
View File

@ -2,21 +2,8 @@ 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',
io = 'cap_io',
)
Mod('loss',
'frappy_psi.parmod.Par',
'loss parameter',
read='cap.loss',
unit='deg',
uri='dil4-ts.psi.ch:3008',
)

View File

@ -4,22 +4,33 @@ Node('ls340test.psi.ch',
)
Mod('io',
'frappy_psi.lakeshore.IO340',
'frappy_psi.lakeshore.Ls340IO',
'communication to ls340',
uri='tcp://localhost:7777'
uri='tcp://ldmprep56-ts:3002'
)
Mod('dev',
'frappy_psi.lakeshore.Device340',
'device for calcurve',
io='io',
curve_handling=True,
)
Mod('T',
'frappy_psi.lakeshore.Sensor340',
'frappy_psi.lakeshore.TemperatureLoop340',
'sample temperature',
# output_module='Heater',
device='dev',
channel='A',
calcurve='x29746',
output_module='Heater',
target=Param(max=470),
io='io',
channel='B'
)
Mod('T_cold_finger',
'frappy_psi.lakeshore.Sensor340',
'cold finger temperature',
io='io',
channel='A'
)
Mod('Heater',
'frappy_psi.lakeshore.HeaterOutput',
'heater output',
channel='B',
io='io',
resistance=25,
max_power=50,
current=1
)

View File

@ -1,17 +0,0 @@
from frappy_psi.ccracks import Rack
Node('ori7test.psi.ch',
'ORI7 test',
'tcp://5000'
)
rack = Rack(Mod)
rack.lakeshore()
rack.sensor('Ts', channel='C', calcurve='x186350')
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
rack.heater('htr', 1, '100W', 25)
rack.ccu(he=True, n2=True)
rack.hepump()

100
debian/changelog vendored
View File

@ -1,4 +1,4 @@
frappy-core (0.20.4) stable; urgency=medium
frappy-core (0.20.4) jammy; urgency=medium
[ Georg Brandl ]
* remove unused file
@ -17,7 +17,7 @@ frappy-core (0.20.4) stable; urgency=medium
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 14 Nov 2024 14:43:54 +0100
frappy-core (0.20.3) stable; urgency=medium
frappy-core (0.20.3) jammy; urgency=medium
[ Georg Brandl ]
* fixup test for cfg_editor utils to run from non-checkout, and fix names, and remove example code
@ -27,7 +27,7 @@ frappy-core (0.20.3) stable; urgency=medium
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 07 Nov 2024 10:57:11 +0100
frappy-core (0.20.2) stable; urgency=medium
frappy-core (0.20.2) jammy; urgency=medium
[ Georg Brandl ]
* pylint: do not try to infer too much
@ -73,7 +73,7 @@ frappy-core (0.20.2) stable; urgency=medium
-- Georg Brandl <jenkins@frm2.tum.de> Wed, 06 Nov 2024 10:40:26 +0100
frappy-core (0.20.1) stable; urgency=medium
frappy-core (0.20.1) jammy; urgency=medium
* gui: do not add a console logger when there is no sys.stdout
* remove unused test class
@ -83,7 +83,7 @@ frappy-core (0.20.1) stable; urgency=medium
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 17 Oct 2024 16:31:27 +0200
frappy-core (0.20.0) stable; urgency=medium
frappy-core (0.20.0) jammy; urgency=medium
[ Alexander Zaft ]
* bin: remove make_doc
@ -128,7 +128,7 @@ frappy-core (0.20.0) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Thu, 17 Oct 2024 14:24:29 +0200
frappy-core (0.19.10) stable; urgency=medium
frappy-core (0.19.10) jammy; urgency=medium
[ Alexander Zaft ]
* debian: let frappy-core replace frappy-demo
@ -138,25 +138,25 @@ frappy-core (0.19.10) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 07 Aug 2024 17:00:06 +0200
frappy-core (0.19.9) stable; urgency=medium
frappy-core (0.19.9) jammy; 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) stable; urgency=medium
frappy-core (0.19.8) jammy; 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) stable; urgency=medium
frappy-core (0.19.7) jammy; 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) stable; urgency=medium
frappy-core (0.19.6) jammy; urgency=medium
[ Jens Krüger ]
* SINQ/SEA: Fix import error due to None value
@ -170,7 +170,7 @@ frappy-core (0.19.6) stable; urgency=medium
-- Jens Krüger <jenkins@frm2.tum.de> Tue, 06 Aug 2024 13:56:51 +0200
frappy-core (0.19.5) stable; urgency=medium
frappy-core (0.19.5) jammy; urgency=medium
* client: fix how to raise error on wrong ident
* add missing requirements to setup.py
@ -179,13 +179,13 @@ frappy-core (0.19.5) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Mon, 05 Aug 2024 09:30:53 +0200
frappy-core (0.19.4) stable; urgency=medium
frappy-core (0.19.4) jammy; 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) stable; urgency=medium
frappy-core (0.19.3) jammy; urgency=medium
[ Markus Zolliker ]
* frappy_psi.extparams.StructParam: fix doc + simplify
@ -205,7 +205,7 @@ frappy-core (0.19.3) stable; urgency=medium
-- Markus Zolliker <jenkins@frm2.tum.de> Fri, 26 Jul 2024 08:36:43 +0200
frappy-core (0.19.2) stable; urgency=medium
frappy-core (0.19.2) jammy; urgency=medium
[ l_samenv ]
* fix missing update after error on parameter
@ -230,7 +230,7 @@ frappy-core (0.19.2) stable; urgency=medium
-- l_samenv <jenkins@frm2.tum.de> Tue, 18 Jun 2024 15:21:43 +0200
frappy-core (0.19.1) stable; urgency=medium
frappy-core (0.19.1) jammy; urgency=medium
[ Markus Zolliker ]
* SecopClient.online must be True while activating
@ -242,7 +242,7 @@ frappy-core (0.19.1) stable; urgency=medium
-- Markus Zolliker <jenkins@frm2.tum.de> Fri, 07 Jun 2024 16:50:33 +0200
frappy-core (0.19.0) stable; urgency=medium
frappy-core (0.19.0) jammy; urgency=medium
[ Markus Zolliker ]
* simulation: extra_params might be a list
@ -298,14 +298,14 @@ frappy-core (0.19.0) stable; urgency=medium
-- Markus Zolliker <jenkins@frm2.tum.de> Thu, 16 May 2024 11:31:25 +0200
frappy-core (0.18.1) stable; urgency=medium
frappy-core (0.18.1) focal; 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) stable; urgency=medium
frappy-core (0.18.0) focal; urgency=medium
[ Alexander Zaft ]
* Add shutdownModule function
@ -416,7 +416,7 @@ frappy-core (0.18.0) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 17 Jan 2024 12:35:00 +0100
frappy-core (0.17.13) stable; urgency=medium
frappy-core (0.17.13) focal; urgency=medium
[ Alexander Zaft ]
* add egg-info to gitignore
@ -437,7 +437,7 @@ frappy-core (0.17.13) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 20 Jun 2023 14:38:00 +0200
frappy-core (0.17.12) stable; urgency=medium
frappy-core (0.17.12) focal; urgency=medium
[ Alexander Zaft ]
* Warn about duplicate module definitions in a file
@ -462,7 +462,7 @@ frappy-core (0.17.12) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 13 Jun 2023 06:51:27 +0200
frappy-core (0.17.11) stable; urgency=medium
frappy-core (0.17.11) focal; urgency=medium
[ Alexander Zaft ]
* Add __format__ to EnumMember
@ -535,7 +535,7 @@ frappy-core (0.17.11) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Thu, 25 May 2023 09:38:24 +0200
frappy-core (0.17.10) stable; urgency=medium
frappy-core (0.17.10) focal; urgency=medium
* Change leftover %-logging calls to lazy
* Convert formatting automatically to f-strings
@ -547,25 +547,25 @@ frappy-core (0.17.10) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 19 Apr 2023 14:32:52 +0200
frappy-core (0.17.9) stable; urgency=medium
frappy-core (0.17.9) focal; 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) stable; urgency=medium
frappy-core (0.17.8) focal; 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) stable; urgency=medium
frappy-core (0.17.7) focal; 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) stable; urgency=medium
frappy-core (0.17.6) focal; urgency=medium
[ Alexander Zaft ]
* gui: show parameter properties again
@ -585,25 +585,25 @@ frappy-core (0.17.6) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 04 Apr 2023 08:42:26 +0200
frappy-core (0.17.5) stable; urgency=medium
frappy-core (0.17.5) focal; urgency=medium
* Fix generator
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 22 Mar 2023 12:32:06 +0100
frappy-core (0.17.4) stable; urgency=medium
frappy-core (0.17.4) focal; 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) stable; urgency=medium
frappy-core (0.17.3) focal; urgency=medium
* UNRELEASED
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Mar 2023 15:55:09 +0100
frappy-core (0.17.2) stable; urgency=medium
frappy-core (0.17.2) focal; urgency=medium
[ Alexander Zaft ]
* Fix Simulation and Proxy
@ -740,7 +740,7 @@ frappy-core (0.17.2) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Mar 2023 15:49:06 +0100
frappy-core (0.17.1) stable; urgency=medium
frappy-core (0.17.1) focal; urgency=medium
[ Georg Brandl ]
* gitignore: ignore demo PID file
@ -759,7 +759,7 @@ frappy-core (0.17.1) stable; urgency=medium
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 17:44:56 +0100
frappy-core (0.17.0) stable; urgency=medium
frappy-core (0.17.0) focal; urgency=medium
[ Alexander Zaft ]
* Rework GUI.
@ -770,37 +770,37 @@ frappy-core (0.17.0) stable; urgency=medium
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Feb 2023 13:52:17 +0100
frappy-core (0.16.1) stable; urgency=medium
frappy-core (0.16.1) focal; urgency=medium
* UNRELEASED
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:44:28 +0100
frappy-core (0.16.4) stable; urgency=medium
frappy-core (0.16.4) focal; urgency=medium
* UNRELEASED
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:09:20 +0100
frappy-core (0.16.3) stable; urgency=medium
frappy-core (0.16.3) focal; urgency=medium
* UNRELEASED
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:00:15 +0100
frappy-core (0.16.2) stable; urgency=medium
frappy-core (0.16.2) focal; 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) stable; urgency=medium
frappy-core (0.16.1) focal; 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) stable; urgency=medium
frappy-core (0.16.0) focal; urgency=medium
[ Enrico Faulhaber ]
* fix sorce package name
@ -862,7 +862,7 @@ frappy-core (0.16.0) stable; urgency=medium
-- Enrico Faulhaber <jenkins@frm2.tum.de> Mon, 20 Feb 2023 16:15:10 +0100
frappy-core (0.15.0) stable; urgency=medium
frappy-core (0.15.0) focal; urgency=medium
[ Björn Pedersen ]
* Remove iohandler left-overs from docs
@ -892,7 +892,7 @@ frappy-core (0.15.0) stable; urgency=medium
-- Björn Pedersen <jenkins@frm2.tum.de> Thu, 10 Nov 2022 14:46:01 +0100
secop-core (0.14.3) stable; urgency=medium
secop-core (0.14.3) focal; urgency=medium
[ Enrico Faulhaber ]
* change repo to secop/frappy
@ -908,13 +908,13 @@ secop-core (0.14.3) stable; urgency=medium
-- Enrico Faulhaber <jenkins@frm2.tum.de> Thu, 03 Nov 2022 13:51:52 +0100
secop-core (0.14.2) stable; urgency=medium
secop-core (0.14.2) focal; 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) stable; urgency=medium
secop-core (0.14.1) focal; urgency=medium
[ Markus Zolliker ]
* secop_psi.entangle.AnalogInput: fix main value
@ -926,7 +926,7 @@ secop-core (0.14.1) stable; urgency=medium
-- Markus Zolliker <jenkins@frm2.tum.de> Thu, 20 Oct 2022 14:04:07 +0200
secop-core (0.14.0) stable; urgency=medium
secop-core (0.14.0) focal; urgency=medium
* add simple interactive python client
* fix undefined status in softcal
@ -940,7 +940,7 @@ secop-core (0.14.0) stable; urgency=medium
-- Markus Zolliker <jenkins@frm2.tum.de> Wed, 19 Oct 2022 11:31:50 +0200
secop-core (0.13.1) stable; urgency=medium
secop-core (0.13.1) focal; urgency=medium
[ Markus Zolliker ]
* an enum with value 0 should be interpreted as False
@ -951,7 +951,7 @@ secop-core (0.13.1) stable; urgency=medium
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 02 Aug 2022 15:31:52 +0200
secop-core (0.13.0) stable; urgency=medium
secop-core (0.13.0) focal; urgency=medium
[ Georg Brandl ]
* debian: fix email addresses in changelog
@ -1014,13 +1014,13 @@ secop-core (0.13.0) stable; urgency=medium
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 02 Aug 2022 09:47:06 +0200
secop-core (0.12.4) stable; urgency=medium
secop-core (0.12.4) focal; 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) stable; urgency=medium
secop-core (0.12.3) focal; urgency=medium
[ Georg Brandl ]
* Makefile: fix docker image
@ -1043,7 +1043,7 @@ secop-core (0.12.3) stable; urgency=medium
-- Georg Brandl <jenkins@jenkins01.admin.frm2.tum.de> Wed, 10 Nov 2021 16:33:19 +0100
secop-core (0.12.2) stable; urgency=medium
secop-core (0.12.2) focal; urgency=medium
[ Markus Zolliker ]
* fix issue with new syntax in simulation
@ -1055,13 +1055,13 @@ secop-core (0.12.2) stable; urgency=medium
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Tue, 18 May 2021 10:29:17 +0200
secop-core (0.12.1) stable; urgency=medium
secop-core (0.12.1) focal; 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) stable; urgency=medium
secop-core (0.12.0) focal; urgency=medium
[ Markus Zolliker ]
* make datatypes immutable

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
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-compat (= 13),
Build-Depends: debhelper (>= 11~),
dh-python,
python3 (>=3.6),
python3-all,
@ -20,7 +20,7 @@ Build-Depends: debhelper-compat (= 13),
git,
markdown,
python3-daemon
Standards-Version: 4.6.2
Standards-Version: 4.1.4
X-Python3-Version: >= 3.6
Package: frappy-core

View File

@ -498,7 +498,7 @@ class Console(code.InteractiveConsole):
history = None
if readline:
try:
history = expanduser(f'~/.config/frappy/{name}-history')
history = expanduser(f'~/.frappy-{name}-history')
readline.read_history_file(history)
except FileNotFoundError:
pass
@ -538,10 +538,10 @@ def init(*nodes):
return success
def interact(usage_tail='', appname=None):
def interact(usage_tail=''):
empty = '_c0' not in clientenv.namespace
print(USAGE.format(
client_name='cli' if empty else '_c0',
client_assign="\ncli = Client('localhost:5000')\n" if empty else '',
tail=usage_tail))
Console(name=f'cli-{appname}' if appname else 'cli')
Console()

View File

@ -95,9 +95,7 @@ class Collector:
self.cls = cls
def add(self, *args, **kwds):
result = self.cls(*args, **kwds)
self.list.append(result)
return result
self.list.append(self.cls(*args, **kwds))
def append(self, mod):
self.list.append(mod)

View File

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

View File

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

View File

@ -1,50 +0,0 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""handling of prefixes of physical units"""
import re
import prefixed
prefixed.SI_MAGNITUDE['u'] = 1e-6 # accept 'u' as replacement for 'µ'
class NumberWithUnit:
def __init__(self, *units):
pfx = "|".join(prefixed.SI_MAGNITUDE)
unt = "|".join(units)
self.units = units
self.pattern = re.compile(rf'\s*([+-]?\d*\.?\d*(?:[eE][+-]?\d+)?\s*(?:{pfx})?)({unt})\s*$')
def parse(self, value):
"""parse and return number and value"""
match = self.pattern.match(value)
if not match:
raise ValueError(f'{value!r} can not be interpreted as a number with unit {",".join(self.units)}')
number, unit = match.groups()
return prefixed.Float(number), unit
def getnum(self, value):
"""parse and return value only"""
return self.parse(value)[0]
def format_with_unit(value, unit='', digits=3):
return f'{prefixed.Float(value):.{digits}H}{unit}'

View File

@ -60,6 +60,7 @@ 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
@ -113,8 +114,8 @@ class HasAccessibles(HasProperties):
wrapped_name = '_' + cls.__name__
for pname, pobj in accessibles.items():
# wrap of reading/writing funcs
if not isinstance(pobj, Parameter) or pobj.optional:
# nothing to do for Commands and optional parameters
if not isinstance(pobj, Parameter):
# nothing to do for Commands
continue
rname = 'read_' + pname
@ -198,15 +199,16 @@ class HasAccessibles(HasProperties):
new_wfunc.__module__ = cls.__module__
cls.wrappedAttributes[wname] = new_wfunc
cls.checkedMethods.update(cls.wrappedAttributes)
# check for programming errors
for attrname, func in cls.__dict__.items():
for attrname in dir(cls):
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.wrappedAttributes
and not hasattr(func, 'poll')): # may be a handler, which always has a poll attribute
if prefix in ('read', 'write') and attrname not in cls.checkedMethods:
raise ProgrammingError(f'{cls.__name__}.{attrname} defined, but {pname!r} is no parameter')
try:
@ -323,7 +325,6 @@ 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
@ -389,8 +390,6 @@ 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)
@ -451,12 +450,9 @@ class Module(HasAccessibles):
self.parameters[name] = accessible
if isinstance(accessible, Command):
self.commands[name] = accessible
if cfg is not None:
if cfg:
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}'")
@ -613,7 +609,7 @@ class Module(HasAccessibles):
# we do not need self.errors any longer. should we delete it?
# del self.errors
if self.polledModules:
self.__poller = mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
self.startModuleDone = True
def initialReads(self):
@ -626,28 +622,8 @@ 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 server shuts down
"""called when the sever shuts down
any cleanup-work should be performed here, like closing threads and
saving data.
@ -750,14 +726,13 @@ class Module(HasAccessibles):
if not polled_modules: # no polls needed - exit thread
return
to_poll = ()
while modules: # modules will be cleared on shutdown
while True:
now = time.time()
wait_time = 999
for mobj in modules:
pinfo = mobj.pollInfo
if pinfo:
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
pinfo.last_slow + mobj.slowinterval - now)
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)
@ -766,7 +741,7 @@ class Module(HasAccessibles):
# call doPoll of all modules where due
for mobj in modules:
pinfo = mobj.pollInfo
if pinfo and now > pinfo.last_main + pinfo.interval:
if now > pinfo.last_main + pinfo.interval:
try:
pinfo.last_main = (now // pinfo.interval) * pinfo.interval
except ZeroDivisionError:
@ -786,7 +761,7 @@ class Module(HasAccessibles):
# collect due slow polls
for mobj in modules:
pinfo = mobj.pollInfo
if pinfo and now > pinfo.last_slow + mobj.slowinterval:
if 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

@ -47,7 +47,6 @@ class Accessible(HasProperties):
"""
ownProperties = None
optional = False
def init(self, kwds):
# do not use self.propertyValues.update here, as no invalid values should be
@ -97,8 +96,6 @@ 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):
@ -194,9 +191,8 @@ class Parameter(Accessible):
readerror = None
omit_unchanged_within = 0
def __init__(self, description=None, datatype=None, inherit=True, optional=False, **kwds):
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
super().__init__()
self.optional = optional
if 'poll' in kwds and generalConfig.tolerate_poll_property:
kwds.pop('poll')
if datatype is None:
@ -230,16 +226,10 @@ class Parameter(Accessible):
def __get__(self, instance, owner):
if instance is None:
return self
try:
return instance.parameters[self.name].value
except KeyError:
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
return instance.parameters[self.name].value
def __set__(self, obj, value):
try:
obj.announceUpdate(self.name, value)
except KeyError:
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
obj.announceUpdate(self.name, value)
def __set_name__(self, owner, name):
self.name = name
@ -376,6 +366,9 @@ 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')
@ -391,9 +384,8 @@ class Command(Accessible):
func = None
def __init__(self, argument=False, *, result=None, inherit=True, optional=False, **kwds):
def __init__(self, argument=False, *, result=None, inherit=True, **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'")
@ -419,8 +411,8 @@ class Command(Accessible):
def __set_name__(self, owner, name):
self.name = name
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')
if self.func is None:
raise ProgrammingError(f'Command {owner.__name__}.{name} must be used as a method decorator')
self.fixExport()
self.datatype = CommandType(self.argument, self.result)

View File

@ -102,6 +102,7 @@ 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,7 +19,6 @@
#
# *****************************************************************************
import time
import traceback
from collections import OrderedDict
@ -256,15 +255,6 @@ 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

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

View File

@ -215,10 +215,7 @@ class HasStates:
self.read_status()
if fast_poll:
sm.reset_fast_poll = True
if fast_poll is True:
self.setFastPoll(True)
else:
self.setFastPoll(True, fast_poll)
self.setFastPoll(True)
self.pollInfo.trigger(True) # trigger poller
def stop_machine(self, stopped_status=(IDLE, 'stopped')):

View File

@ -1,397 +1,313 @@
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Damaris Tartarotti Maimone
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""Wrapper for the ADQ data acquisition card for ultrasound"""
import sys
import atexit
import signal
import time
import numpy as np
import ctypes as ct
from scipy.signal import butter, filtfilt
# For different trigger modes
SW_TRIG = 1
# The following external trigger does not work if the level of the trigger is very close to 0.5V.
# Now we have it close to 3V, and it works
EXT_TRIG_1 = 2
EXT_TRIG_2 = 7
EXT_TRIG_3 = 8
LVL_TRIG = 3
INT_TRIG = 4
LVL_FALLING = 0
LVL_RISING = 1
ADQ_CLOCK_INT_INTREF = 0 # internal clock source
ADQ_CLOCK_EXT_REF = 1 # internal clock source, external reference
ADQ_CLOCK_EXT_CLOCK = 2 # External clock source
ADQ_TRANSFER_MODE_NORMAL = 0x00
ADQ_CHANNELS_MASK = 0x3
GHz = 1e9
RMS_TO_VPP = 2 * np.sqrt(2)
class Timer:
def __init__(self):
self.data = [(time.time(), 'start')]
def __call__(self, text=''):
now = time.time()
prev = self.data[-1][0]
self.data.append((now, text))
return now - prev
def summary(self):
return ' '.join(f'{txt} {tim:.3f}' for tim, txt in self.data[1:])
def show(self):
first = prev = self.data[0][0]
print('---', first)
for tim, txt in self.data[1:]:
print(f'{(tim - first) * 1000:9.3f} {(tim - prev) * 1000:9.3f} ms {txt}')
prev = tim
class Adq:
sample_rate = 2 * GHz
max_number_of_channels = 2
ndecimate = 50 # decimation ratio (2GHz / 40 MHz)
number_of_records = 1
samples_per_record = 16384
bw_cutoff = 10E6
trigger = EXT_TRIG_1
adq_num = 1
data = None
busy = False
def __init__(self):
global ADQAPI
ADQAPI = ct.cdll.LoadLibrary("libadq.so.0")
ADQAPI.ADQAPI_GetRevision()
# Manually set return type from some ADQAPI functions
ADQAPI.CreateADQControlUnit.restype = ct.c_void_p
ADQAPI.ADQ_GetRevision.restype = ct.c_void_p
ADQAPI.ADQ_GetPtrStream.restype = ct.POINTER(ct.c_int16)
ADQAPI.ADQControlUnit_FindDevices.argtypes = [ct.c_void_p]
# Create ADQControlUnit
self.adq_cu = ct.c_void_p(ADQAPI.CreateADQControlUnit())
ADQAPI.ADQControlUnit_EnableErrorTrace(self.adq_cu, 3, '.')
# Find ADQ devices
ADQAPI.ADQControlUnit_FindDevices(self.adq_cu)
n_of_adq = ADQAPI.ADQControlUnit_NofADQ(self.adq_cu)
if n_of_adq != 1:
print('number of ADQs must be 1, not %d' % n_of_adq)
print('it seems the ADQ was not properly closed')
print('please try again or reboot')
sys.exit(0)
atexit.register(self.deletecu)
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num)
revision = ct.cast(rev, ct.POINTER(ct.c_int))
out = [f'Connected to ADQ #1, FPGA Revision: {revision[0]}']
if revision[1]:
out.append('Local copy')
else:
if revision[2]:
out.append('SVN Managed - Mixed Revision')
else:
out.append('SVN Updated')
print(', '.join(out))
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF)
##########################
# Test pattern
# ADQAPI.ADQ_SetTestPatternMode(self.adq_cu, self.adq_num, 4)
##########################
# Sample skip
# ADQAPI.ADQ_SetSampleSkip(self.adq_cu, self.adq_num, 1)
##########################
# set trigger mode
if not ADQAPI.ADQ_SetTriggerMode(self.adq_cu, self.adq_num, self.trigger):
raise RuntimeError('ADQ_SetTriggerMode failed.')
if self.trigger == LVL_TRIG:
if not ADQAPI.ADQ_SetLvlTrigLevel(self.adq_cu, self.adq_num, -100):
raise RuntimeError('ADQ_SetLvlTrigLevel failed.')
if not ADQAPI.ADQ_SetTrigLevelResetValue(self.adq_cu, self.adq_num, 1000):
raise RuntimeError('ADQ_SetTrigLevelResetValue failed.')
if not ADQAPI.ADQ_SetLvlTrigChannel(self.adq_cu, self.adq_num, 1):
raise RuntimeError('ADQ_SetLvlTrigChannel failed.')
if not ADQAPI.ADQ_SetLvlTrigEdge(self.adq_cu, self.adq_num, LVL_RISING):
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
elif self.trigger == EXT_TRIG_1:
if not ADQAPI.ADQ_SetExternTrigEdge(self.adq_cu, self.adq_num, 2):
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
# if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)):
# raise RuntimeError('SetTriggerThresholdVoltage failed.')
# proabably the folloiwng is wrong.
# print("CHANNEL:" + str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
def init(self, samples_per_record=None, number_of_records=None):
"""initialize dimensions and store result object"""
if samples_per_record:
self.samples_per_record = samples_per_record
if number_of_records:
self.number_of_records = number_of_records
# Setup target buffers for data
self.target_buffers = (ct.POINTER(ct.c_int16 * self.samples_per_record * self.number_of_records)
* self.max_number_of_channels)()
for bufp in self.target_buffers:
bufp.contents = (ct.c_int16 * self.samples_per_record * self.number_of_records)()
def deletecu(self):
cu = self.__dict__.pop('adq_cu', None)
if cu is None:
return
print('shut down ADQ')
# Only disarm trigger after data is collected
ADQAPI.ADQ_DisarmTrigger(cu, self.adq_num)
ADQAPI.ADQ_MultiRecordClose(cu, self.adq_num)
# Delete ADQControlunit
ADQAPI.DeleteADQControlUnit(cu)
print('ADQ closed')
def start(self, data):
# Start acquisition
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
self.number_of_records,
self.samples_per_record)
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
self.data = data
def get_data(self):
"""get new data if available"""
ready = False
data = self.data
if not data:
self.busy = False
return None # no new data
if ADQAPI.ADQ_GetAcquiredAll(self.adq_cu, self.adq_num):
ready = True
data.timer('ready')
else:
if self.trigger == SW_TRIG:
ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num)
if not ready:
self.busy = True
return None
self.data = None
t = time.time()
# Get data from ADQ
if not ADQAPI.ADQ_GetData(
self.adq_cu, self.adq_num, self.target_buffers,
self.samples_per_record * self.number_of_records, 2,
0, self.number_of_records, ADQ_CHANNELS_MASK,
0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL):
raise RuntimeError('no success from ADQ_GetDATA')
data.retrieve(self)
return data
class PEdata:
def __init__(self, adq):
self.sample_rate = adq.sample_rate
self.samp_freq = self.sample_rate / GHz
self.number_of_records = adq.number_of_records
self.timer = Timer()
def retrieve(self, adq):
data = []
rawsignal = []
for ch in range(2):
onedim = np.frombuffer(adq.target_buffers[ch].contents, dtype=np.int16)
rawsignal.append(onedim[:adq.samples_per_record])
# convert 16 bit int to a value in the range -1 .. 1
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2 ** 15))
# Now this is an array with all records, but the time is artificial
self.data = data
self.rawsignal = rawsignal
self.timer('retrieved')
def sinW(self, sig, freq, ti, tf):
# sig: signal array
# freq
# ti, tf: initial and end time
si = int(ti * self.samp_freq)
nperiods = freq * (tf - ti)
n = int(round(max(2, int(nperiods)) / nperiods * (tf-ti) * self.samp_freq))
self.nperiods = n
t = np.arange(si, len(sig)) / self.samp_freq
t = t[:n]
self.pulselen = n / self.samp_freq
sig = sig[si:si+n]
a = 2*np.sum(sig*np.cos(2*np.pi*freq*t))/len(sig)
b = 2*np.sum(sig*np.sin(2*np.pi*freq*t))/len(sig)
return a, b
def mix(self, sigin, sigout, freq, ti, tf):
# sigin, sigout: signal array, incomping, output
# freq
# ti, tf: initial and end time of sigin
a, b = self.sinW(sigin, freq, ti, tf)
amp = np.sqrt(a**2 + b**2)
a, b = a/amp, b/amp
# si = int(ti * self.samp_freq)
t = np.arange(len(sigout)) / self.samp_freq
wave1 = sigout * (a * np.cos(2*np.pi*freq*t) + b * np.sin(2*np.pi*freq*t))
wave2 = sigout * (a * np.sin(2*np.pi*freq*t) - b * np.cos(2*np.pi*freq*t))
return wave1, wave2
def averageiq(self, data, freq, ti, tf):
"""Average over records"""
iorq = np.array([self.mix(data[0][i], data[1][i], freq, ti, tf) for i in range(self.number_of_records)])
return iorq.sum(axis=0) / self.number_of_records
def filtro(self, iorq, cutoff):
# butter lowpass
nyq = 0.5 * self.sample_rate
normal_cutoff = cutoff / nyq
order = 5
b, a = butter(order, normal_cutoff, btype='low', analog=False)
iqf = [filtfilt(b, a, iorq[i]) for i in np.arange(len(iorq))]
return iqf
def box(self, iorq, ti, tf):
si = int(self.samp_freq * ti)
sf = int(self.samp_freq * tf)
bxa = [sum(iorq[i][si:sf])/(sf-si) for i in np.arange(len(iorq))]
return bxa
def gates_and_curves(self, freq, pulse, roi, bw_cutoff):
"""return iq values of rois and prepare plottable curves for iq"""
self.timer('gates')
try:
self.ndecimate = int(round(self.sample_rate / freq))
except TypeError as e:
raise TypeError(f'{self.sample_rate}/{freq} {e}')
iq = self.averageiq(self.data, freq / GHz, *pulse)
self.timer('aviq')
iqf = self.filtro(iq, bw_cutoff)
self.timer('filtro')
m = max(1, len(iqf[0]) // self.ndecimate)
ll = m * self.ndecimate
iqf = [iqfx[0:ll] for iqfx in iqf]
self.timer('iqf')
iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2)
self.timer('avg')
t_axis = np.arange(m) * self.ndecimate / self.samp_freq
pulsig = np.abs(self.data[0][0])
self.timer('pulsig')
pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1)
result = ([self.box(iqf, *r) for r in roi], # gates
(t_axis, iqd[0], iqd[1], pulsig)) # curves
self.timer('result')
# self.timer.show()
# ns = len(self.rawsignal[0]) * self.number_of_records
# print(f'{ns} {ns / 2e6} ms')
return result
class Namespace:
"""holds channel or other data"""
def __init__(self, **kwds):
self.__dict__.update(**kwds)
class RUSdata:
def __init__(self, adq, freq, periods, delay_samples):
self.sample_rate = adq.sample_rate
self.freq = freq
self.periods = periods
self.delay_samples = delay_samples
self.samples_per_record = adq.samples_per_record
self.inp = Namespace(idx=0, name='input')
self.out = Namespace(idx=1, name='output')
self.channels = (self.inp, self.out)
self.timer = Timer()
def retrieve(self, adq):
self.timer('start retrieve')
npts = self.samples_per_record - self.delay_samples
nbin = max(1, npts // (self.periods * 60)) # for performance reasons, do the binning first
nreduced = npts // nbin
ft = 2 * np.pi * self.freq * nbin / self.sample_rate * np.arange(nreduced)
self.timer('create time axis')
# complex_sinusoid = np.exp(1j * ft) # do not use this, below is 33 % faster
complex_sinusoid = 1j * np.sin(ft) + np.cos(ft)
self.timer('sinusoid')
rawsignal = [] # for raw plot
for chan in self.channels: # looping over input and output
# although the ADC is only 14 bit it is represented as unsigend 16 bit numbers,
# and due to some calculations (calibration) the last 2 bits are not zero
beg = self.delay_samples
isignal = np.frombuffer(adq.target_buffers[chan.idx].contents, dtype=np.int16)[beg:beg+nreduced * nbin]
self.timer('isignal')
reduced = isignal.reshape((-1, nbin)).mean(axis=1) # this converts also int16 to float
self.timer('reduce')
rawsignal.append(reduced)
chan.signal = signal = reduced * 2 ** -16 # in V -> peak to peak 1 V ~ +- 0.5 V
self.timer('divide')
# calculate RMS * sqrt(2) -> peak sinus amplitude.
# may be higher than the input range by a factor 1.4 when heavily clipped
chan.amplitude = np.sqrt((signal ** 2).mean()) * RMS_TO_VPP
self.timer('amp')
chan.mixed = signal * complex_sinusoid
self.timer('mix')
chan.mean = chan.mixed.mean()
self.timer('mean')
self.rawsignal = rawsignal
if self.inp.mean:
self.iq = self.out.mean / self.inp.mean
else:
self.iq = 0
def get_quality(self):
"""get signal quality info
quality info (small values indicate good quality):
- input_stddev:
the imaginary part indicates deviations in phase
the real part indicates deviations in amplitude
- output_slope:
the imaginary part indicates a turning phase (rad/sec)
the real part indicates changes in amplitude (0.01 ~= 1%/sec)
"""
self.timer('get_quality')
npts = len(self.channels[0].signal)
nper = npts // self.periods
for chan in self.channels:
mean = chan.mixed.mean()
chan.reduced = chan.mixed[:self.periods * nper].reshape((-1, nper)).mean(axis=1) / mean
timeaxis = np.arange(len(self.out.reduced)) * self.sample_rate / self.freq
result = Namespace(
input_stddev=self.inp.reduced.std(),
output_slope=np.polyfit(timeaxis, self.out.reduced, 1)[0])
self.timer('got_quality')
self.timer.show()
ns = len(self.rawsignal[0])
print(f'{ns} {ns / 2e6} ms')
return result
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Damaris Tartarotti Maimone
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""Wrapper for the ADQ data acquisition card for ultrasound"""
import sys
import atexit
import signal
import time
import numpy as np
import ctypes as ct
from scipy.signal import butter, filtfilt
# For different trigger modes
SW_TRIG = 1
# The following external trigger does not work if the level of the trigger is very close to 0.5V.
# Now we have it close to 3V, and it works
EXT_TRIG_1 = 2
EXT_TRIG_2 = 7
EXT_TRIG_3 = 8
LVL_TRIG = 3
INT_TRIG = 4
LVL_FALLING = 0
LVL_RISING = 1
ADQ_CLOCK_INT_INTREF = 0 # internal clock source
ADQ_CLOCK_EXT_REF = 1 # internal clock source, external reference
ADQ_CLOCK_EXT_CLOCK = 2 # External clock source
ADQ_TRANSFER_MODE_NORMAL = 0x00
ADQ_CHANNELS_MASK = 0x3
GHz = 1e9
class Adq:
sample_rate = 2 * GHz
max_number_of_channels = 2
ndecimate = 50 # decimation ratio (2GHz / 40 MHz)
number_of_records = 1
samples_per_record = 16384
bw_cutoff = 10E6
trigger = EXT_TRIG_1
adq_num = 1
UNDEFINED = -1
IDLE = 0
BUSY = 1
READY = 2
status = UNDEFINED
data = None
def __init__(self):
global ADQAPI
ADQAPI = ct.cdll.LoadLibrary("libadq.so.0")
ADQAPI.ADQAPI_GetRevision()
# Manually set return type from some ADQAPI functions
ADQAPI.CreateADQControlUnit.restype = ct.c_void_p
ADQAPI.ADQ_GetRevision.restype = ct.c_void_p
ADQAPI.ADQ_GetPtrStream.restype = ct.POINTER(ct.c_int16)
ADQAPI.ADQControlUnit_FindDevices.argtypes = [ct.c_void_p]
# Create ADQControlUnit
self.adq_cu = ct.c_void_p(ADQAPI.CreateADQControlUnit())
ADQAPI.ADQControlUnit_EnableErrorTrace(self.adq_cu, 3, '.')
# Find ADQ devices
ADQAPI.ADQControlUnit_FindDevices(self.adq_cu)
n_of_adq = ADQAPI.ADQControlUnit_NofADQ(self.adq_cu)
if n_of_adq != 1:
raise RuntimeError('number of ADQs must be 1, not %d' % n_of_adq)
rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num)
revision = ct.cast(rev, ct.POINTER(ct.c_int))
print('\nConnected to ADQ #1')
# Print revision information
print('FPGA Revision: {}'.format(revision[0]))
if revision[1]:
print('Local copy')
else:
print('SVN Managed')
if revision[2]:
print('Mixed Revision')
else:
print('SVN Updated')
print('')
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF)
##########################
# Test pattern
# ADQAPI.ADQ_SetTestPatternMode(self.adq_cu, self.adq_num, 4)
##########################
# Sample skip
# ADQAPI.ADQ_SetSampleSkip(self.adq_cu, self.adq_num, 1)
##########################
# set trigger mode
if not ADQAPI.ADQ_SetTriggerMode(self.adq_cu, self.adq_num, self.trigger):
raise RuntimeError('ADQ_SetTriggerMode failed.')
if self.trigger == LVL_TRIG:
if not ADQAPI.ADQ_SetLvlTrigLevel(self.adq_cu, self.adq_num, -100):
raise RuntimeError('ADQ_SetLvlTrigLevel failed.')
if not ADQAPI.ADQ_SetTrigLevelResetValue(self.adq_cu, self.adq_num, 1000):
raise RuntimeError('ADQ_SetTrigLevelResetValue failed.')
if not ADQAPI.ADQ_SetLvlTrigChannel(self.adq_cu, self.adq_num, 1):
raise RuntimeError('ADQ_SetLvlTrigChannel failed.')
if not ADQAPI.ADQ_SetLvlTrigEdge(self.adq_cu, self.adq_num, LVL_RISING):
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
elif self.trigger == EXT_TRIG_1:
if not ADQAPI.ADQ_SetExternTrigEdge(self.adq_cu, self.adq_num, 2):
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
# if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)):
# raise RuntimeError('SetTriggerThresholdVoltage failed.')
print("CHANNEL:"+str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
atexit.register(self.deletecu)
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
def init(self, samples_per_record=None, number_of_records=None):
"""initialize dimensions"""
if samples_per_record:
self.samples_per_record = samples_per_record
if number_of_records:
self.number_of_records = number_of_records
# Setup target buffers for data
self.target_buffers = (ct.POINTER(ct.c_int16 * self.samples_per_record * self.number_of_records)
* self.max_number_of_channels)()
for bufp in self.target_buffers:
bufp.contents = (ct.c_int16 * self.samples_per_record * self.number_of_records)()
def deletecu(self):
# Only disarm trigger after data is collected
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
ADQAPI.ADQ_MultiRecordClose(self.adq_cu, self.adq_num)
# Delete ADQControlunit
ADQAPI.DeleteADQControlUnit(self.adq_cu)
def start(self):
# Start acquisition
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
self.number_of_records,
self.samples_per_record)
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
self.status = self.BUSY
def get_status(self):
"""check if ADQ card is busy"""
if self.status == self.BUSY:
if ADQAPI.ADQ_GetAcquiredAll(self.adq_cu, self.adq_num):
self.status = self.READY
else:
if self.trigger == SW_TRIG:
ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num)
return self.status
def get_data(self, dataclass, **kwds):
"""when ready, get raw data from card, else return cached data
return
"""
if self.get_status() == self.READY:
# Get data from ADQ
if not ADQAPI.ADQ_GetData(self.adq_cu, self.adq_num, self.target_buffers,
self.samples_per_record * self.number_of_records, 2,
0, self.number_of_records, ADQ_CHANNELS_MASK,
0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL):
raise RuntimeError('no success from ADQ_GetDATA')
self.data = dataclass(self, **kwds)
self.status = self.IDLE
if self.status == self.UNDEFINED:
raise RuntimeError('no data available yet')
return self.data
class PEdata:
def __init__(self, adq):
self.sample_rate = adq.sample_rate
self.samp_freq = self.sample_rate / GHz
self.number_of_records = adq.number_of_records
data = []
for ch in range(2):
onedim = np.frombuffer(adq.target_buffers[ch].contents, dtype=np.int16)
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2**14)) # 14 bits ADC
# Now this is an array with all records, but the time is artificial
self.data = data
def sinW(self, sig, freq, ti, tf):
# sig: signal array
# freq
# ti, tf: initial and end time
si = int(ti * self.samp_freq)
nperiods = freq * (tf - ti)
n = int(round(max(2, int(nperiods)) / nperiods * (tf-ti) * self.samp_freq))
self.nperiods = n
t = np.arange(si, len(sig)) / self.samp_freq
t = t[:n]
self.pulselen = n / self.samp_freq
sig = sig[si:si+n]
a = 2*np.sum(sig*np.cos(2*np.pi*freq*t))/len(sig)
b = 2*np.sum(sig*np.sin(2*np.pi*freq*t))/len(sig)
return a, b
def mix(self, sigin, sigout, freq, ti, tf):
# sigin, sigout: signal array, incomping, output
# freq
# ti, tf: initial and end time of sigin
a, b = self.sinW(sigin, freq, ti, tf)
amp = np.sqrt(a**2 + b**2)
a, b = a/amp, b/amp
# si = int(ti * self.samp_freq)
t = np.arange(len(sigout)) / self.samp_freq
wave1 = sigout * (a * np.cos(2*np.pi*freq*t) + b * np.sin(2*np.pi*freq*t))
wave2 = sigout * (a * np.sin(2*np.pi*freq*t) - b * np.cos(2*np.pi*freq*t))
return wave1, wave2
def averageiq(self, data, freq, ti, tf):
"""Average over records"""
iorq = np.array([self.mix(data[0][i], data[1][i], freq, ti, tf) for i in range(self.number_of_records)])
return iorq.sum(axis=0) / self.number_of_records
def filtro(self, iorq, cutoff):
# butter lowpass
nyq = 0.5 * self.sample_rate
normal_cutoff = cutoff / nyq
order = 5
b, a = butter(order, normal_cutoff, btype='low', analog=False)
iqf = [filtfilt(b, a, iorq[i]) for i in np.arange(len(iorq))]
return iqf
def box(self, iorq, ti, tf):
si = int(self.samp_freq * ti)
sf = int(self.samp_freq * tf)
bxa = [sum(iorq[i][si:sf])/(sf-si) for i in np.arange(len(iorq))]
return bxa
def gates_and_curves(self, freq, pulse, roi, bw_cutoff):
"""return iq values of rois and prepare plottable curves for iq"""
self.ndecimate = int(round(self.sample_rate / freq))
# times = []
# times.append(('aviq', time.time()))
iq = self.averageiq(self.data, freq / GHz, *pulse)
# times.append(('filtro', time.time()))
iqf = self.filtro(iq, bw_cutoff)
m = len(iqf[0]) // self.ndecimate
ll = m * self.ndecimate
iqf = [iqfx[0:ll] for iqfx in iqf]
# times.append(('iqdec', time.time()))
iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2)
t_axis = np.arange(m) * self.ndecimate / self.samp_freq
pulsig = np.abs(self.data[0][0])
# times.append(('pulsig', time.time()))
pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1)
self.curves = (t_axis, iqd[0], iqd[1], pulsig)
# print(times)
return [self.box(iqf, *r) for r in roi]
class RUSdata:
def __init__(self, adq, freq, periods):
self.sample_rate = adq.sample_rate
self.freq = freq
self.periods = periods
self.samples_per_record = adq.samples_per_record
input_signal = np.frombuffer(adq.target_buffers[0].contents, dtype=np.int16)
output_signal = np.frombuffer(adq.target_buffers[1].contents, dtype=np.int16)
complex_sinusoid = np.exp(1j * 2 * np.pi * self.freq / self.sample_rate * np.arange(len(input_signal)))
self.input_mixed = input_signal * complex_sinusoid
self.output_mixed = output_signal * complex_sinusoid
self.input_mean = self.input_mixed.mean()
self.output_mean = self.output_mixed.mean()
self.iq = self.output_mean / self.input_mean
def get_reduced(self, mixed):
"""get reduced array and normalize"""
nper = self.samples_per_record // self.periods
mean = mixed.mean()
return mixed[:self.period * nper].reshape((-1, nper)).mean(axis=0) / mean
def calc_quality(self):
"""get signal quality info
quality info (small values indicate good quality):
- input_std and output_std:
the imaginary part indicates deviations in phase
the real part indicates deviations in amplitude
- input_slope and output_slope:
the imaginary part indicates a turning phase (rad/sec)
the real part indicates changes in amplitude (0.01 ~= 1%/sec)
"""
reduced = self.get_reduced(self.input_mixed)
self.input_stdev = reduced.std()
reduced = self.get_reduced(self.output_mixed)
timeaxis = np.arange(len(reduced)) * self.sample_rate / self.freq
self.output_slope = np.polyfit(timeaxis, reduced, 1)[0]

View File

@ -1,131 +0,0 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import 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

@ -1,128 +0,0 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# 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

@ -22,7 +22,6 @@
import os
import re
from pathlib import Path
from os.path import basename, dirname, exists, join
import numpy as np
@ -32,22 +31,13 @@ from scipy.interpolate import PchipInterpolator, CubicSpline, PPoly # pylint: d
from frappy.errors import ProgrammingError, RangeError
from frappy.lib import clamp
def identity(x):
return x
def exp10(x):
return 10 ** np.array(x)
to_scale = {
'lin': identity,
'log': np.log10,
'lin': lambda x: x,
'log': lambda x: np.log10(x),
}
from_scale = {
'lin': identity,
'log': exp10,
'lin': lambda x: x,
'log': lambda x: 10 ** np.array(x),
}
TYPES = [ # lakeshore type, inp-type, loglog
('DT', 'si', False), # Si diode
@ -65,7 +55,7 @@ TYPES = [ # lakeshore type, inp-type, loglog
OPTION_TYPE = {
'loglog': 0, # boolean
'extrange': 2, # tuple(min T, max T) for extrapolation
'extrange': 2, # tuple(min T, max T for extrapolation
'calibrange': 2, # tuple(min T, max T)
}
@ -232,6 +222,14 @@ PARSERS = {
}
def check(x, y, islog):
# check interpolation error
yi = y[:-2] + (x[1:-1] - x[:-2]) * (y[2:] - y[:-2]) / (x[2:] - x[:-2])
if islog:
return sum((yi - y[1:-1]) ** 2)
return sum((np.log10(yi) - np.log10(y[1:-1])) ** 2)
def get_curve(newscale, curves):
"""get curve from curve cache (converts not existing ones)
@ -249,7 +247,6 @@ def get_curve(newscale, curves):
class CalCurve(HasOptions):
EXTRAPOLATION_AMOUNT = 0.1
MAX_EXTRAPOLATION_FACTOR = 2
filename = None # calibration file
def __init__(self, calibspec=None, *, x=None, y=None, cubic_spline=True, **options):
"""calibration curve
@ -260,7 +257,7 @@ class CalCurve(HasOptions):
[<full path> | <name>][,<key>=<value> ...]
for <key>/<value> as in parser arguments
:param x, y: x and y arrays (given instead of calibspec)
:param cubic_spline: set to False for always using Pchip interpolation
:param cubic_split: set to False for always using Pchip interpolation
:param options: options for parsers
"""
self.options = options
@ -268,31 +265,26 @@ class CalCurve(HasOptions):
parser = StdParser()
parser.xdata = x
parser.ydata = y
self.calibname = 'custom'
else:
if x or y:
raise ProgrammingError('can not give both calibspec and x,y ')
sensopt = calibspec.split(',')
calibname = sensopt.pop(0)
self.calibname = basename(calibname)
head, dot, ext = self.calibname.rpartition('.')
if dot:
self.calibname = head
_, dot, ext = basename(calibname).rpartition('.')
kind = None
pathlist = [Path(p.strip()) for p in os.environ.get('FRAPPY_CALIB_PATH', '').split(':')]
pathlist.append(Path(dirname(__file__)) / 'calcurves')
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(':')
pathlist.append(join(dirname(__file__), 'calcurves'))
for path in pathlist:
# first try without adding kind
filename = path / calibname
if filename.exists():
filename = join(path.strip(), calibname)
if exists(filename):
kind = ext if dot else None
break
# then try adding all kinds as extension
for nam in calibname, calibname.upper(), calibname.lower():
for kind in PARSERS:
filename = path / f'{nam}.{kind}'
filename = join(path.strip(), '%s.%s' % (nam, kind))
if exists(filename):
self.filename = filename
break
else:
continue
@ -336,7 +328,6 @@ class CalCurve(HasOptions):
not_incr_idx = np.argwhere(x[1:] <= x[:-1])
if len(not_incr_idx):
raise RangeError('x not monotonic at x=%.4g' % x[not_incr_idx[0]])
self.ptc = y[-1] > y[0]
self.x = {parser.xscale: x}
self.y = {parser.yscale: y}
@ -353,7 +344,8 @@ class CalCurve(HasOptions):
self.convert_x = to_scale[newscale]
self.convert_y = from_scale[newscale]
self.calibrange = self.options.get('calibrange')
self.extra_points = (0, 0)
dirty = set()
self.extra_points = False
self.cutted = False
if self.calibrange:
self.calibrange = sorted(self.calibrange)
@ -379,6 +371,7 @@ class CalCurve(HasOptions):
self.y = {newscale: y}
ibeg = 0
iend = len(x)
dirty.add('xy')
else:
self.extra_points = ibeg, len(x) - iend
else:
@ -500,48 +493,13 @@ class CalCurve(HasOptions):
except IndexError:
return defaultx
def interpolation_error(self, x0, x1, y0, y1, funx, funy, relerror, return_tuple=False):
"""calcualte interpoaltion error
:param x0: start of interval
:param x1: end of interval
:param y0: y at start of interval
:param y1: y at end of interval
:param funx: function to convert x from exported scale to internal scale
:param funy: function to convert y from internal scale to exported scale
:param relerror: True when the exported y scale is linear
:param return_tuple: True: return interpolation error as a tuple with two values
(without and with 3 additional points)
False: return one value without additional points
:return: relative deviation
"""
xspace = np.linspace(x0, x1, 9)
x = funx(xspace)
yr = self.spline(x)
yspline = funy(yr)
yinterp = y0 + np.linspace(0.0, y1 - y0, 9)
# difference between spline (at m points) and liner interpolation
diff = np.abs(yspline - yinterp)
# estimate of interpolation error with 4 sections:
# difference between spline (at m points) and linear interpolation between neighboring points
if relerror:
fact = 2 / (np.abs(y0) + np.abs(y1)) # division by zero can not happen, as y0 and y1 can not both be zero
else:
fact = 2.3 # difference is in log10 -> multiply by 1 / log10(e)
result = np.max(diff, axis=0) * fact
if return_tuple:
diff2 = np.abs(0.5 * (yspline[:-2:2] + yspline[2::2]) - funy(yr[1:-1:2]))
return result, np.max(diff2, axis=0) * fact
return result
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None, nmin=199):
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None):
"""export curve for downloading to hardware
:param nmax: max number of points. if the number of given points is bigger,
the points with the lowest interpolation error are omitted
:param logformat: a list with two elements of None, True or False for x and y
True: use log, False: use lin, None: use log if self.loglog
:param logformat: a list with two elements of None, True or False
True: use log, False: use line, None: use log if self.loglog
values None are replaced with the effectively used format
False / True are replaced by [False, False] / [True, True]
default is False
@ -549,26 +507,25 @@ class CalCurve(HasOptions):
:param extrapolate: a flag indicating whether the curves should be extrapolated
to the preset extrapolation range
:param xlimits: max x range
:param nmin: minimum number of points
:return: numpy array with 2 dimensions returning the curve
"""
if logformat in (True, False):
logformat = (logformat, logformat)
self.logformat = list(logformat)
logformat = [logformat, logformat]
try:
scales = []
for idx, logfmt in enumerate(logformat):
if logfmt and self.lin_forced[idx]:
raise ValueError('%s must contain positive values only' % 'xy'[idx])
self.logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
scales.append('log' if linlog else 'lin')
xscale, yscale = scales
except (TypeError, AssertionError):
raise ValueError('logformat must be a 2 element sequence or a boolean')
raise ValueError('logformat must be a 2 element list or a boolean')
xr = self.spline.x[1:-1] # raw units, excluding extrapolated points
x1, x2 = xmin, xmax = xr[0], xr[-1]
x = self.spline.x[1:-1] # raw units, excluding extrapolated points
x1, x2 = xmin, xmax = x[0], x[-1]
y1, y2 = sorted(self.spline([x1, x2]))
if extrapolate and not yrange:
yrange = self.exty
@ -578,100 +535,42 @@ class CalCurve(HasOptions):
lim = to_scale[self.scale](xlimits)
xmin = clamp(xmin, *lim)
xmax = clamp(xmax, *lim)
# start and end index of calibrated range
ibeg, iend = self.extra_points[0], len(xr) - self.extra_points[1]
if xmin != x1 or xmax != x2:
i, j = np.searchsorted(xr, (xmin, xmax))
if abs(xr[i] - xmin) < 0.1 * (xr[i + 1] - xr[i]):
ibeg, iend = np.searchsorted(x, (xmin, xmax))
if abs(x[ibeg] - xmin) < 0.1 * (x[ibeg + 1] - x[ibeg]):
# remove first point, if close
i += 1
if abs(xr[j - 1] - xmax) < 0.1 * (xr[j - 1] - xr[j - 2]):
ibeg += 1
if abs(x[iend - 1] - xmax) < 0.1 * (x[iend - 1] - x[iend - 2]):
# remove last point, if close
j -= 1
offset = i - 1
xr = np.concatenate(([xmin], xr[i:j], [xmax]))
ibeg = max(0, ibeg - offset)
iend = min(len(xr), iend - offset)
yr = self.spline(xr)
iend -= 1
x = np.concatenate(([xmin], x[ibeg:iend], [xmax]))
y = self.spline(x)
# convert to exported scale
if xscale == self.scale:
xbwd = identity
x = xr
else:
if self.scale == 'log':
xfwd, xbwd = from_scale[self.scale], to_scale[self.scale]
else:
xfwd, xbwd = to_scale[xscale], from_scale[xscale]
x = xfwd(xr)
if yscale == self.scale:
yfwd = identity
y = yr
else:
if self.scale == 'log':
yfwd = from_scale[self.scale]
else:
yfwd = to_scale[yscale]
y = yfwd(yr)
if xscale != self.scale:
x = to_scale[xscale](from_scale[self.scale](x))
if yscale != self.scale:
y = to_scale[yscale](from_scale[self.scale](y))
self.deviation = None
nmin = min(nmin, nmax)
# reduce number of points, if needed
n = len(x)
relerror = yscale == 'lin'
if len(x) > nmax:
# reduce number of points, if needed
i, j = 1, n - 1 # index range for calculating interpolation deviation
deviation = np.zeros(n)
while True:
deviation[i:j] = self.interpolation_error(
x[i-1:j-1], x[i+1:j+1], y[i-1:j-1], y[i+1:j+1],
xbwd, yfwd, relerror)
# calculate interpolation error when a single point is omitted
if n <= nmax:
break
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
y = np.delete(y, idx)
x = np.delete(x, idx)
deviation = np.delete(deviation, idx)
n = len(x)
# index range to recalculate
i, j = max(1, idx - 1), min(n - 1, idx + 1)
self.deviation = deviation # for debugging purposes
elif n < nmin:
if ibeg + 1 < iend:
diff1, diff4 = self.interpolation_error(
x[ibeg:iend - 1], x[ibeg + 1:iend], y[ibeg:iend - 1], y[ibeg + 1:iend],
xbwd, yfwd, relerror, return_tuple=True)
dif_target = 1e-4
sq4 = np.sqrt(diff4) * 4
sq1 = np.sqrt(diff1)
offset = 0.49
n_mid = nmax - len(x) + iend - ibeg - 1
# iteration to find a dif target resulting in no more than nmax points
while True:
scale = 1 / np.sqrt(dif_target)
# estimate number of intermediate points (float!) needed to reach dif_target
# number of points estimated from the result of the interpolation error with 4 sections
n4 = np.maximum(1, sq4 * scale)
# number of points estimated from the result of the interpolation error with 1 section
n1 = np.maximum(1, sq1 * scale)
# use n4 where n4 > 4, n1, where n1 < 1 and a weighted average in between
nn = np.select([n4 > 4, n1 > 1],
[n4, (n4 * (n1 - 1) + n1 * (4 - n4)) / (3 + n1 - n4)], n1)
n_tot = np.sum(np.rint(nn + offset))
extra = n_tot - n_mid
if extra <= 0:
break
dif_target *= (n_tot / n_mid) ** 2
xnew = [x[:ibeg]]
for x0, x1, ni in zip(x[ibeg:iend-1], x[ibeg+1:iend], np.rint(nn + offset)):
xnew.append(np.linspace(x0, x1, int(ni) + 1)[:-1])
xnew.append(x[iend-1:])
x = np.concatenate(xnew)
y = yfwd(self.spline(xbwd(x)))
# for debugging purposes:
self.deviation = self.interpolation_error(x[:-1], x[1:], y[:-1], y[1:], xbwd, yfwd, relerror)
i, j = 1, n - 1 # index range for calculating interpolation deviation
deviation = np.zeros(n)
while True:
# calculate interpolation error when a single point is omitted
ym = y[i-1:j-1] + (x[i:j] - x[i-1:j-1]) * (y[i+1:j+1] - y[i-1:j-1]) / (x[i+1:j+1] - x[i-1:j-1])
if yscale == 'log':
deviation[i:j] = np.abs(ym - y[i:j])
else:
deviation[i:j] = np.abs(ym - y[i:j]) / (np.abs(ym + y[i:j]) + 1e-10)
if n <= nmax:
break
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
y = np.delete(y, idx)
x = np.delete(x, idx)
deviation = np.delete(deviation, idx)
n -= 1
# index range to recalculate
i, j = max(1, idx - 1), min(n - 1, idx + 1)
self.deviation = deviation # for debugging purposes
return np.stack([x, y], axis=1)

View File

@ -1,139 +0,0 @@
import os
from glob import glob
from pathlib import Path
from configparser import ConfigParser
from frappy.errors import ConfigError
class Rack:
configbase = Path('/home/l_samenv/.config/frappy_instruments')
def __init__(self, modfactory, **kwds):
self.modfactory = modfactory
instpath = self.configbase / os.environ['Instrument']
sections = {}
self.config = {}
files = glob(str(instpath / '*.ini'))
for filename in files:
parser = ConfigParser()
parser.optionxform = str
parser.read([filename])
for section in parser.sections():
prev = sections.get(section)
if prev:
raise ConfigError(f'duplicate {section} section in {filename} and {prev}')
sections[section] = filename
self.config.update(parser.items(section))
if 'rack' not in sections:
raise ConfigError(f'no rack found in {instpath}')
self.props = {} # dict (<property>, <method>) of value
self.mods = {} # dict (<property>, <method>) of list of <cfg>
def set_props(self, mod, **kwds):
for prop, method in kwds.items():
value = self.props.get((prop, method))
if value is None:
# add mod to the list of cfgs to be fixed
self.mods.setdefault((prop, method), []).append(mod)
else:
# set prop in current module
if not mod.get(prop): # do not override given and not empty property
mod[prop] = value
def fix_props(self, method, **kwds):
for prop, value in kwds.items():
if (prop, method) in self.props:
raise ConfigError(f'duplicate call to {method}()')
self.props[prop, method] = value
# set property in modules to be fixed
for mod in self.mods.get((prop, method), ()):
mod[prop] = value
def lakeshore(self, ls_uri=None, io='ls_io', dev='ls', model='336', **kwds):
Mod = self.modfactory
self.fix_props('lakeshore', io=io, device=dev)
self.ls_model = model
self.ls_dev = dev
ls_uri = ls_uri or self.config.get('ls_uri')
Mod(io, cls=f'frappy_psi.lakeshore.IO{self.ls_model}',
description='comm. to lakeshore in cc rack', uri=ls_uri)
self.dev = Mod(dev, cls=f'frappy_psi.lakeshore.Device{self.ls_model}',
description='lakeshore in cc rack', io=io, curve_handling=True)
def sensor(self, name, channel, calcurve, **kwds):
Mod = self.modfactory
kwds.setdefault('cls', f'frappy_psi.lakeshore.Sensor{self.ls_model}')
kwds.setdefault('description', f'T sensor {name}')
mod = Mod(name, channel=channel, calcurve=calcurve,
device=self.ls_dev, **kwds)
self.set_props(mod, io='lakeshore', dev='lakeshore')
def loop(self, name, channel, calcurve, output_module, **kwds):
Mod = self.modfactory
kwds.setdefault('cls', f'frappy_psi.lakeshore.Loop{self.ls_model}')
kwds.setdefault('description', f'T loop {name}')
Mod(name, channel=channel, calcurve=calcurve, output_module=output_module,
device=self.ls_dev, **kwds)
self.fix_props(f'heater({output_module})', description=f'heater for {name}')
def heater(self, name, output_no, max_heater, resistance, **kwds):
Mod = self.modfactory
if output_no == 1:
kwds.setdefault('cls', f'frappy_psi.lakeshore.MainOutput{self.ls_model}')
elif output_no == 2:
kwds.setdefault('cls', f'frappy_psi.lakeshore.SecondaryOutput{self.ls_model}')
else:
return
kwds.setdefault('description', '')
mod = Mod(name, max_heater=max_heater, resistance=resistance, **kwds)
self.set_props(mod, io='lakeshore', device='lakeshore', description=f'heater({name})')
def ccu(self, ccu_uri=None, ccu_io='ccu_io', he=None, n2=None, **kwds):
Mod = self.modfactory
self.ccu_io = ccu_io
ccu_uri = ccu_uri or self.config.get('ccu_uri')
# self.devname = ccu_devname
Mod(ccu_io, 'frappy_psi.ccu4.IO',
'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.ccu_io)
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=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 hepump(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):
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)
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

@ -23,30 +23,30 @@
import time
import math
from frappy.lib.enum import Enum
from frappy.lib import clamp
# 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, nopoll
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
StatusType, IntRange, StringType, TupleOf
from frappy.dynamic import Pinata
from frappy.errors import CommunicationFailedError
from frappy.states import HasStates, status_code, Retry
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=4, no_motor=5)
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=5, no_motor=6)
A = Enum(disabled=0, manual=1, auto=2)
class IO(StringIO):
class CCU4IO(StringIO):
"""communication with CCU4"""
# for completeness: (not needed, as it is the default)
end_of_line = '\n'
# on connect, we send 'cid' and expect a reply starting with 'CCU4'
identification = [('cid', r'cid=CCU4.*')]
identification = [('cid', r'CCU4.*')]
class Base(HasIO):
ioClass = IO
class CCU4Base(HasIO):
ioClass = CCU4IO
def command(self, **kwds):
"""send a command and get the response
@ -80,7 +80,7 @@ class Base(HasIO):
return result
class HeLevel(Base, Readable):
class HeLevel(CCU4Base, Readable):
"""He Level channel of CCU4"""
value = Parameter(unit='%')
@ -122,10 +122,10 @@ class HeLevel(Base, Readable):
return self.command(hfu=value)
class Valve(Base, Writable):
class Valve(CCU4Base, Writable):
value = Parameter('relay state', BoolType())
target = Parameter('relay target', BoolType())
ioClass = IO
ioClass = CCU4IO
STATE_MAP = {0: (0, (IDLE, 'off')),
1: (1, (IDLE, 'on')),
2: (0, (ERROR, 'no valve')),
@ -144,7 +144,7 @@ class Valve(Base, Writable):
self.command(**self._close_command)
def read_status(self):
state = int(self.command(**self._query_state))
state = self.command(self._query_state)
self.value, status = self.STATE_MAP[state]
return status
@ -174,14 +174,14 @@ class N2TempSensor(Readable):
value = Parameter('LN2 T sensor', FloatRange(unit='K'), default=0)
class N2Level(Base, Readable):
class N2Level(CCU4Base, Pinata, Readable):
valve = Attached(Writable, mandatory=False)
lower = Attached(Readable, mandatory=False)
upper = Attached(Readable, mandatory=False)
value = Parameter('vessel state', EnumType(empty=0, ok=1, full=2))
status = Parameter(datatype=StatusType(Readable, 'DISABLED', 'BUSY'))
mode = Parameter('auto mode', EnumType(A), readonly=False, default=A.manual)
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
mode = Parameter('auto mode', EnumType(A), readonly=False)
threshold = Parameter('threshold triggering start/stop filling',
FloatRange(unit='K'), readonly=False)
@ -206,6 +206,15 @@ class N2Level(Base, Readable):
5: (WARN, 'empty'),
}
def scanModules(self):
for modname, name in self.names.items():
if name:
sensor_name = name.replace('$', self.name)
self.setProperty(modname, sensor_name)
yield sensor_name, {
'cls': N2FillValve if modname == 'valve' else N2TempSensor,
'description': f'LN2 {modname} T sensor'}
def initialReads(self):
self.command(nav=1) # tell CCU4 to activate LN2 sensor readings
super().initialReads()
@ -271,335 +280,151 @@ class N2Level(Base, Readable):
@Command()
def fill(self):
"""start filling"""
self.mode = A.auto
self.command(nc=1)
self.io.write(nc=1)
@Command()
def stop(self):
"""stop filling"""
if self.mode == A.auto:
# set to watching
self.command(nc=3)
else:
# set to off
self.command(nc=0)
self.io.write(nc=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):
class FlowPressure(CCU4Base, Readable):
value = Parameter(unit='mbar')
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)
mbar_offset = Parameter(unit='mbar', default=0.8, readonly=False)
pollinterval = Parameter(default=0.25)
def read_value(self):
return self.filter(self.filter_time, self.command(f=float)) - self.mbar_offset
return self.filter(self.command(f=float)) - self.mbar_offset
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)
class NeedleValve(HasStates, CCU4Base, Drivable):
flow = Attached(Readable, mandatory=False)
flow_pressure = Attached(Readable, mandatory=False)
value = Parameter(unit='ln/min')
target = Parameter(unit='ln/min')
motor_state = Parameter('motor_state', EnumType(M), default=0)
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, default=0.001)
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False)
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, readonly=False)
control_active = Parameter('control active flag', BoolType(), readonly=False, default=1)
min_open_step = Parameter('minimal open step', FloatRange(unit='s'), readonly=False, default=0.06)
min_close_step = Parameter('minimal close step', FloatRange(unit='s'), readonly=False, default=0.05)
raw_open_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.12)
raw_close_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.04)
pollinterval = Parameter(default=5)
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)
_ref_time = 0
_ref_dif = 0
_dir = 0
_rawdir = 0
_last_cycle = 0
_last_progress = 0
_step = 0
def initModule(self):
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()
def update_from_flow(self, value):
if not self.use_pressure:
self.value = value
# self.cycle_machine()
def update_from_pressure(self, value):
if self.use_pressure:
self.value = value * self.lnm_per_mbar
# self.cycle_machine()
# def doPoll(self):
# """only the updates should trigger the machine"""
def read_value(self):
p = self.pressure.read_value() * self.lnm_per_mbar
f = self.flow_sensor.read_value()
# self.log.info('p %g f %g +- %.2g', p, f, self.flow_sensor.stddev)
self.read_motor_state()
return p if self.use_pressure else f
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.pressure, 'tolerance'):
self.pressure.tolerance = tolerance / self.lnm_per_mbar
if hasattr(self.flow_sensor, 'tolerance'):
self.flow_sensor.tolerance = 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 read_use_pressure(self):
if self.pressure:
if self.flow_sensor:
if self.flow_pressure:
if self.flow:
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.change_target, fast_poll=1)
self.start_machine(self.controlling, in_tol_time=0,
ref_time=0, ref_dif=0, prev_dif=0)
@status_code(BUSY)
def change_target(self, state):
state.in_tol_time = 0
state.last_minstep = {}
state.last_progress = state.now
state.ref_time = 0
state.ref_dif = 0
state.prev_dif = 0 # used?
state.last_close_time = 0
state.last_pulse_time = 0
state.raw_fact = 1
state.raw_step = 0
if abs(self.target - self.value) < self._tolerance():
return self.at_target
return self.raw_control
def start_direction(self, state):
if self.target > self.value:
self._dir = 1
state.minstep = self.min_open_step
else:
self._dir = -1
state.minstep = self.min_close_step
state.prev = []
def perform_pulse(self, state):
tol = self._tolerance()
dif = self.target - self.value
difdir = dif * self._dir
state.last_pulse_time = state.now
if difdir > tol:
step = state.minstep + (difdir - tol) * self.prop
elif difdir > 0:
step = state.minstep * difdir / tol
else:
return
self.log.info('MP %g dif=%g tol=%g', step * self._dir, dif, tol)
self.command(mp=clamp(-1, step * self._dir, 1))
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 raw_control(self, state):
tol = self._tolerance()
if state.init:
self.start_direction(state)
state.raw_step = self.raw_open_step if self._dir > 0 else -self.raw_close_step
state.raw_fact = 1
# if self.read_motor_state() == M.closed:
# # TODO: also check for flow near lower limit ? but only once after change_target
# self.log.info('start with fast opening')
# state.raw_step = 1
# self._dir = 1
difdir = (self.target - self.value) * self._dir
state.prev.append(difdir)
state.diflim = max(0, difdir - tol * 1)
state.success = 0
self.command(mp=state.raw_step)
self.log.info('first rawstep %g', state.raw_step)
state.last_pulse_time = state.now
state.raw_pulse_cnt = 0
state.cycle_cnt = 0
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
difdir = (self.target - self.value) * self._dir
state.cycle_cnt += 1
state.prev.append(difdir)
del state.prev[:-5]
if state.prev[-1] > max(state.prev[:-1]):
# TODO: use the amount of overshoot to reduce the raw_step
state.cycle_cnt = 0
self.log.info('difference is increasing %s', ' '.join(f'{v:g}' for v in state.prev))
if self.motor_state == 'idle':
self.command(mp=state.wiggle)
return Retry
if state.cycle_cnt >= 5:
state.cycle_cnt = 0
state.diflim = max(tol, min(state.prev) - tol * 0.5)
state.raw_pulse_cnt += 1
self.command(mp=state.raw_step * state.raw_fact)
self.log.info('rawstep %g', state.raw_step)
if state.raw_pulse_cnt % 5 == 0 and state.raw_pulse_cnt > 5:
state.raw_fact *= 1.25
return Retry
if difdir >= state.diflim:
state.success = max(0, state.success - 1)
return Retry
state.success += 1
if state.success <= 3:
return Retry
if state.raw_pulse_cnt < 3:
state.raw_fact = 1 - (3 - state.raw_pulse_cnt) ** 2 * 0.05
if state.raw_fact != 1:
if self._dir > 0:
self.raw_open_step *= state.raw_fact
self.log.info('raw_open_step %g f=%g', self.raw_open_step, state.raw_fact)
self.min_open_pulse = min(self.min_open_pulse, self.raw_open_step)
else:
self.raw_close_step *= state.raw_fact
self.log.info('raw_close_step %g f=%g', self.raw_close_step, state.raw_fact)
self.min_close_pulse = min(self.min_close_pulse, self.raw_close_step)
if self.motor_state == 'opened':
if state.now < state.start_wiggle + 20:
return Retry
return self.final_status(ERROR, 'can not open')
return self.controlling
# @status_code(BUSY)
# def raw_control(self, state):
# tol = self._tolerance()
# if state.init:
# self.start_direction(state)
# if self._dir != self._rawdir:
# self._rawdir = self._dir
# state.first_step = self.first_open_step if self._dir > 0 else -self.first_close_step
# else:
# state.first_step = 0
# state.first_fact = 1
# # if self.read_motor_state() == M.closed:
# # # TODO: also check for flow near lower limit ? but only once after change_target
# # self.log.info('start with fast opening')
# # state.first_step = 1
# # self._dir = 1
# difdir = (self.target - self.value) * self._dir
# state.prev = [difdir]
# state.diflim = max(0, difdir - tol * 0.5)
# state.success = 0
# if state.first_step:
# self.command(mp=state.first_step)
# else:
# self.perform_pulse(state)
# self.log.info('firststep %g', state.first_step)
# state.last_pulse_time = state.now
# state.raw_pulse_cnt = 0
# return Retry
# difdir = (self.target - self.value) * self._dir
# if state.delta(5):
# state.diflim = max(0, min(state.prev) - tol * 0.1)
# state.prev = [difdir]
# state.raw_pulse_cnt += 1
# if state.first_step and state.raw_pulse_cnt % 10 == 0:
# self.command(mp=state.first_step * state.first_fact)
# self.log.info('repeat firststep %g', state.first_step * state.first_fact)
# state.first_fact *= 1.25
# else:
# self.perform_pulse(state)
# return Retry
# state.prev.append(difdir)
# if difdir >= state.diflim:
# state.success = max(0, state.success - 1)
# return Retry
# state.success += 1
# if state.success <= 5:
# return Retry
# if state.first_step:
# if state.raw_pulse_cnt < 3:
# state.first_fact = 1 - (3 - state.raw_pulse_cnt) ** 2 * 0.04
# if state.first_fact != 1:
# if self._dir > 0:
# self.first_open_step *= state.first_fact
# self.log.info('first_open_step %g f=%g', self.first_open_step, state.first_fact)
# else:
# self.first_close_step *= state.first_fact
# self.log.info('first_close_step %g f=%g', self.first_close_step, state.first_fact)
# return self.controlling
@status_code(BUSY)
def controlling(self, state):
dif = self.target - self.value
if state.init:
self.start_direction(state)
state.ref_dif = abs(dif)
state.ref_time = state.now
state.in_tol_time = 0
difdir = dif * self._dir # negative when overshoot happend
# difdif = dif - state.prev_dif
# state.prev_dif = dif
expected_dif = state.ref_dif * math.exp((state.ref_time - state.now) / self.deriv)
tol = self._tolerance()
if difdir < tol:
# prev_minstep = state.last_minstep.pop(self._dir, None)
# attr = 'min_open_step' if self._dir > 0 else 'min_close_step'
# if prev_minstep is not None:
# # increase minstep
# minstep = getattr(self, attr)
# setattr(self, attr, minstep * 1.1)
# self.log.info('increase %s to %g', attr, minstep)
if difdir > -tol: # within tolerance
delta = state.delta()
state.in_tol_time += delta
if state.in_tol_time > self.settle:
# state.last_minstep.pop(self._dir, None)
self.log.info('at target %g %g', dif, tol)
return self.at_target
if difdir < 0:
return Retry
# self.log.info('minstep=0 dif=%g', dif)
else: # overshoot
self.log.info('overshoot %g', dif)
return self.raw_control
# # overshoot
# prev_minstep = state.last_minstep.pop(self._dir, None)
# if prev_minstep is None:
# minstep = getattr(self, attr) * 0.9
# self.log.info('decrease %s to %g', attr, minstep)
# setattr(self, attr, minstep)
# self.start_step(state, self.target)
# still approaching
if difdir <= expected_dif:
if difdir < expected_dif / 1.25 - tol:
state.ref_time = state.now
state.ref_dif = (difdir + tol) * 1.25
# self.log.info('new ref %g', state.ref_dif)
state.last_progress = state.now
return Retry # progressing: no pulse needed
if state.now < state.last_pulse_time + 2.5:
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
# TODO: check motor state for closed / opened ?
self.perform_pulse(state)
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 _tolerance(self):
return min(self.tolerance * min(1, self.value / 2), self.tolerance2)
@ -609,67 +434,48 @@ class NeedleValveFlow(HasStates, Base, Drivable):
dif = self.target - self.value
if abs(dif) > self._tolerance():
state.in_tol_time = 0
self.log.info('unstable %g', dif)
return self.unstable
return Retry
@status_code(IDLE, 'unstable')
def unstable(self, state):
difdir = (self.target - self.value) * self._dir
if difdir < 0 or self._dir == 0:
return self.raw_control
return self.controlling(state)
def read_motor_state(self):
return self.command(fm=int)
@Command
def close(self):
"""close valve fully"""
self.command(mp=-60)
@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)
self.start_machine(self.closing)
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
@status_code(BUSY)
def closing(self, state):
if state.init:
state.start_time = state.now
self.read_motor_state()
if self.motor_state == M.closing:
tolerance = self._tolerance()
if abs(dif) < tolerance:
state.in_tol_time += delta
if state.in_tol_time > self.settle:
return self.at_target
return Retry
if self.motor_state == M.closed:
return self.final_status(IDLE, 'closed')
if state.now < state.start_time + 1:
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
return Retry
return self.final_status(IDLE, 'fixed')
@Command
def open(self):
"""open valve fully"""
self.command(mp=60)
self.read_motor_state()
self.start_machine(self.opening)
@status_code(BUSY)
def opening(self, state):
if state.init:
state.start_time = state.now
self.read_motor_state()
if self.motor_state == M.opening:
return Retry
if self.motor_state == M.opened:
return self.final_status(IDLE, 'opened')
if state.now < state.start_time + 1:
return Retry
return self.final_status(IDLE, 'fixed')
@Command(FloatRange())
def pulse(self, value):
"""perform a motor pulse"""
self.log.info('pulse %g', value)
self.command(mp=value)
if value > 0:
self.motor_state = M.opening
return self.opening
self.motor_state = M.closing
return self.closing
self.command(mp=state.step)
return Retry

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(upper)
relais = self.actions.get(action.upper())
if relais:
relais.write_target(upper == action) # True when capital letter
else:

View File

@ -1,71 +0,0 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from frappy.core import BoolType, FloatRange, Parameter, Readable, Writable, Attached, EnumType, nopoll
from frappy_psi.trinamic import Motor
from frappy_psi.ccu4 import Pressure, NeedleValveFlow
class ValveMotor(Motor):
has_inputs = True
class HePump(Writable):
valvemotor = Attached(Motor)
flow = Attached(NeedleValveFlow)
valve = Attached(Writable)
value = Parameter(datatype=BoolType())
target = Parameter(datatype=BoolType())
pump_type = Parameter('pump type', EnumType(no=0, neodry=1, xds35=2, sv65=3), readonly=False, default=0)
eco_mode = Parameter('eco mode', BoolType(), readonly=False)
has_feedback = Parameter('feedback works', BoolType(), readonly=False, default=True)
FLOW_SCALE = {'no': 0, 'neodry': 0.55, 'xds35': 0.6, 'sv65': 0.9}
def write_target(self, value):
self.valvemotor.write_output0(value)
def read_target(self):
return self.valvemotor.read_output0()
def read_value(self):
if self.has_feedback:
return not self.valvemotor.read_input3()
return self.target
def write_pump_type(self, value):
self.flow.pressure_scale = self.FLOW_SCALE[value.name]
def read_eco_mode(self):
if self.pump_type == 'xds35':
return self.valvemotor.read_output1()
return False
def write_eco_mode(self, value):
if self.pump_type == 'xds35':
return self.valvemotor.write_output1(value)
# else silently ignore

View File

@ -18,39 +18,16 @@
# Jael Celia Lorenzana <jael-celia.lorenzana@psi.ch>
# *****************************************************************************
import os
from glob import glob
from frappy.core import Readable, Writable, Parameter, BoolType, StringType,\
FloatRange, Property, TupleOf, ERROR, IDLE
from frappy.errors import ConfigError
from math import log
basepaths = '/sys/class/ionopimax', '/sys/class/ionopi'
class Base:
addr = Property('address', StringType())
_devpath = None
devclass = None
def initModule(self):
super().initModule()
# candidates = glob(f'/sys/class/iono*/*/{self.addr}')
# if not candidates:
# raise ConfigError(f'can not find path for {self.addr}')
for basepath in basepaths:
for devclass in ([self.devclass] if isinstance(self.devclass, str) else self.devclass):
devpath = f'{basepath}/{devclass}'
if os.path.exists(devpath):
self._devpath = devpath
return
else:
self.log.info('%s does not exist', devpath)
else:
raise ConfigError(f'device path for {self.devclass} not found {devpath}')
def read(self, addr, scale=None):
with open(f'{self._devpath}/{addr}') as f:
with open(f'/sys/class/ionopimax/{self.devclass}/{addr}') as f:
result = f.read()
if scale:
return float(result) / scale
@ -58,7 +35,7 @@ class Base:
def write(self, addr, value, scale=None):
value = str(round(value * scale)) if scale else str(value)
with open(f'{self._devpath}/{addr}', 'w') as f:
with open(f'/sys/class/ionopimax/{self.devclass}/{addr}', 'w') as f:
f.write(value)
@ -72,7 +49,7 @@ class DigitalInput(Base, Readable):
class DigitalOutput(DigitalInput, Writable):
target = Parameter('output state', BoolType(), readonly=False)
devclass = 'digital_out', 'relay'
devclass = 'digital_out'
def write_target(self, value):
self.write(self.addr, value, 1)

View File

@ -1,65 +1,16 @@
# *****************************************************************************
#
# 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"""
"""
Created on Tue Feb 4 11:07:56 2020
@author: tartarotti_d-adm
"""
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)]
@ -68,18 +19,13 @@ def rects(intervals, y12):
result.append(rect(*x12, *y12))
return np.concatenate(result, axis=1)
class Plot:
def __init__(self, maxy):
self.lines = {}
self.yaxis = ((-2 * maxy, maxy), (-maxy, 2 * maxy))
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
@ -122,7 +68,6 @@ 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])
self.ax = [axleft, axleft.twinx()]
@ -150,8 +95,7 @@ class Plot:
plt.tight_layout()
finally:
self.first = False
plt.draw()
# TODO: do not know why this is needed:
self.fig.canvas.draw()
self.fig.canvas.flush_events()

File diff suppressed because it is too large Load Diff

View File

@ -278,12 +278,7 @@ 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()
try:
self.range, self.iexc, self.vexc = rng, iexc, vexc
except Exception:
# avoid raising errors on disabled channel
if self.enabled:
raise
self.range, self.iexc, self.vexc = rng, iexc, vexc
@CommonWriteHandler(rdgrng_params)
def write_rdgrng(self, change):

View File

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

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

@ -1,96 +0,0 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""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

@ -26,8 +26,7 @@ import struct
from frappy.core import BoolType, Command, EnumType, FloatRange, IntRange, \
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done, \
IDLE, BUSY, ERROR, Limit, nopoll, ArrayOf
from frappy.properties import HasProperties
IDLE, BUSY, ERROR, Limit
from frappy.io import BytesIO
from frappy.errors import CommunicationFailedError, HardwareError, RangeError, IsBusyError
from frappy.rwhandler import ReadHandler, WriteHandler
@ -120,6 +119,9 @@ 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')
@ -130,20 +132,6 @@ 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()
@ -157,18 +145,6 @@ 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
@ -426,6 +402,13 @@ 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"""
@ -476,47 +459,3 @@ 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,13 +25,11 @@ import time
import numpy as np
from frappy_psi.adq_mr import Adq, PEdata, RUSdata
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.core import Attached, BoolType, Done, FloatRange, HasIO, \
IntRange, Module, Parameter, Readable, Writable, Drivable, StringIO, StringType, \
IDLE, BUSY, DISABLED, ERROR, TupleOf, ArrayOf, Command
from frappy.properties import Property
from frappy.lib import clamp
# from frappy.modules import Collector
#from frappy.modules import Collector
Collector = Readable
@ -48,12 +46,11 @@ 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('i, q', TupleOf(FloatRange(), FloatRange()), default=(0, 0))
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)
size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
@ -83,44 +80,6 @@ class Roi(Readable):
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
if freq != self._freq_target:
self._freq_target = freq
# do no control 2 times after changing frequency
self._skipctrl = 2
if self.control_active:
if self._old:
newfreq = freq + inphase * self.slope
fdif = freq - self._old[0]
if abs(fdif) >= self.minstep:
idif = inphase - self._old[1]
self.slope = - fdif / idif
else:
# do a 'test' step
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_active:
self._freq_target = self.freq.write_target(clamp(freq - self.maxstep, newfreq, freq + self.maxstep))
class Pars(Module):
description = 'relevant parameters from SEA'
@ -131,75 +90,36 @@ class Pars(Module):
class FreqStringIO(StringIO):
end_of_line = '\r\n'
end_of_line = '\r'
class Frequency(HasIO, Drivable):
class Frequency(HasIO, Writable):
value = Parameter('frequency', unit='Hz')
amp = Parameter('amplitude (VPP)', FloatRange(unit='V'), readonly=False)
output = Parameter('output: L or R', EnumType(L=1, R=0), readonly=False, default='L')
amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False)
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._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
self.communicate('FREQ %.15g;FREQ?' % value)
self.last_change = time.time()
if self.dif:
self.dif.read_value()
def write_amp(self, amp):
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
reply = self.communicate('AMPR %g;AMPR?' % amp)
return float(reply)
def read_amp(self):
if time.time() > self._nopoll_until or self.amp == 0:
return float(self.communicate(f'AMP{self.output.name}? VPP'))
return self.amp
reply = self.communicate('AMPR?')
return float(reply)
class FrequencyDif(Drivable):
class FrequencyDif(Readable):
freq = Attached(Frequency)
base = Parameter('base frequency', FloatRange(unit='Hz'), default=0)
value = Parameter('difference to base frequency', FloatRange(unit='Hz'), default=0)
@ -208,114 +128,55 @@ class FrequencyDif(Drivable):
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.value - self.base
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
def read_status(self):
return self.freq.read_status()
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
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)
class PulseEcho(Base):
value = Parameter("t, i, q, pulse curves",
TupleOf(*[ArrayOf(FloatRange(), 0, 16283) for _ in range(4)]), default=[[]] * 4)
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.time, r.time + r.size) 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)
@ -329,170 +190,124 @@ class PulseEcho(Base, Readable):
def register_roi(self, roi):
self.roilist.append(roi)
def go(self):
self.starttime = time.time()
self.adq.start()
CONTINUE = 0
GO = 1
DONE_GO = 2
WAIT_GO = 3
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]
class RUS(Base, Collector):
freq = Attached()
imod = Attached(mandatory=False)
qmod = Attached(mandatory=False)
class RUS(Base):
value = Parameter('averaged (I, Q) tuple', TupleOf(FloatRange(), 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)
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())
_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
starttime = None
_data_args = 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 update_freq_target(self, value):
self.go()
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)
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
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
iq = data.iq * self.scale
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
@Command
def go(self):
"""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.starttime = time.time()
freq = self.freq.value
self._data_args = (RUSdata, freq, self.periods)
self.sr = round(self.periods * self.adq.sample_rate / freq)
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)
self.adq.init(self.sr, 1)
self.adq.start()
self.read_status()
class RUSqual(RUS):
"""version with additional info about quality of input and output signal"""
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)
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)
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
# 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))

View File

@ -96,7 +96,7 @@ def print_commit(line):
print(' '.join(output), title)
cnt[0] += 1
if cnt[0] % 50 == 0:
if input(f' {br0:11s} {br1:11s}--- press any letter to continue, return to stop ---') == '':
if input(f' {br0:11s} {br1:11s}'):
raise StopIteration()

View File

@ -1,76 +0,0 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""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

@ -23,6 +23,8 @@
import sys
import threading
import importlib
from glob import glob
import pytest
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
@ -920,6 +922,27 @@ 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, Module
from frappy.modulebase import HasAccessibles
from frappy.params import Command, Parameter
@ -149,105 +149,3 @@ 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()