update to gerrit version
Change-Id: Ifdaa28dd961a529cd9197c4c3639744f108b0a6a
This commit is contained in:
parent
ff6a98af92
commit
aa7910c28c
@ -31,6 +31,7 @@ sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from mlzlog import ColoredConsoleHandler
|
from mlzlog import ColoredConsoleHandler
|
||||||
|
|
||||||
from frappy.gui.qt import QApplication
|
from frappy.gui.qt import QApplication
|
||||||
from frappy.gui.cfg_editor.mainwindow import MainWindow
|
from frappy.gui.cfg_editor.mainwindow import MainWindow
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
@ -23,8 +22,8 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import sys
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
# Add import path for inplace usage
|
# Add import path for inplace usage
|
||||||
@ -61,8 +60,9 @@ def parseArgv(argv):
|
|||||||
action='store',
|
action='store',
|
||||||
help="comma separated list of cfg files,\n"
|
help="comma separated list of cfg files,\n"
|
||||||
"defaults to <name_of_the_instance>.\n"
|
"defaults to <name_of_the_instance>.\n"
|
||||||
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
|
"cfgfiles given without '.cfg' extension are searched"
|
||||||
"else they are treated as path names",
|
" in the configuration directory,"
|
||||||
|
" else they are treated as path names",
|
||||||
default=None)
|
default=None)
|
||||||
parser.add_argument('-g',
|
parser.add_argument('-g',
|
||||||
'--gencfg',
|
'--gencfg',
|
||||||
@ -96,15 +96,13 @@ def main(argv=None):
|
|||||||
generalConfig.init(args.gencfg)
|
generalConfig.init(args.gencfg)
|
||||||
logger.init(loglevel)
|
logger.init(loglevel)
|
||||||
|
|
||||||
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles,
|
||||||
|
interface=args.port, testonly=args.test)
|
||||||
|
|
||||||
if args.daemonize:
|
if args.daemonize:
|
||||||
srv.start()
|
srv.start()
|
||||||
else:
|
else:
|
||||||
try:
|
|
||||||
srv.run()
|
srv.run()
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
43
cfg/seop_cfg.py
Normal file
43
cfg/seop_cfg.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
description = """
|
||||||
|
3He system in Lab ...
|
||||||
|
"""
|
||||||
|
Node('mlz_seop',
|
||||||
|
description,
|
||||||
|
'tcp://10767',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('cell',
|
||||||
|
'frappy_mlz.seop.Cell',
|
||||||
|
'interface module to the driver',
|
||||||
|
config_directory = '/home/jcns/daemon/config',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('afp',
|
||||||
|
'frappy_mlz.seop.Afp',
|
||||||
|
'controls the afp flip of the cell',
|
||||||
|
cell = 'cell'
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('nmr',
|
||||||
|
'frappy_mlz.seop.Nmr',
|
||||||
|
'controls the ',
|
||||||
|
cell = 'cell'
|
||||||
|
)
|
||||||
|
|
||||||
|
fitparams = [
|
||||||
|
('amplitude', 'V'),
|
||||||
|
('T1', 's'),
|
||||||
|
('T2', 's'),
|
||||||
|
('b', ''),
|
||||||
|
('frequency', 'Hz'),
|
||||||
|
('phase', 'deg'),
|
||||||
|
]
|
||||||
|
for param, unit in fitparams:
|
||||||
|
Mod(f'nmr_{param.lower()}',
|
||||||
|
'frappy_mlz.seop.FitParam',
|
||||||
|
f'fittet parameter {param} of NMR',
|
||||||
|
cell = 'cell',
|
||||||
|
value = Param(unit=unit),
|
||||||
|
sigma = Param(unit=unit),
|
||||||
|
param = param,
|
||||||
|
)
|
46
debian/changelog
vendored
46
debian/changelog
vendored
@ -1,3 +1,49 @@
|
|||||||
|
frappy-core (0.17.13) focal; urgency=medium
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* add egg-info to gitignore
|
||||||
|
|
||||||
|
[ Markus Zolliker ]
|
||||||
|
* GUI bugfix: use isChecked instead of checkState in BoolInput
|
||||||
|
* frappy_psi.mercury/triton: add control_off command
|
||||||
|
* frappy_psi.phytron: rename reset_error to clear_errors
|
||||||
|
* frappy.mixins.HasOutputModule
|
||||||
|
* frappy_psi.mercury: proper handling of control_active
|
||||||
|
* add a hook for reads to be done initially
|
||||||
|
* frappy_psi.triton: fix HeaterOutput.limit
|
||||||
|
* frappy_psi.magfield: bug fix
|
||||||
|
* frappy_psi.sea: bug fixes
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* server: fix systemd variable scope
|
||||||
|
|
||||||
|
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 20 Jun 2023 14:38:00 +0200
|
||||||
|
|
||||||
|
frappy-core (0.17.12) focal; urgency=medium
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* Warn about duplicate module definitions in a file
|
||||||
|
* Add influences property to parameters/commands
|
||||||
|
|
||||||
|
[ Markus Zolliker ]
|
||||||
|
* frappy.client: dummy logger is missing 'exception' method
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* Add SEOP He3-polarization device
|
||||||
|
* Typo in influences description
|
||||||
|
* seop: fix fitparam command
|
||||||
|
|
||||||
|
[ Markus Zolliker ]
|
||||||
|
* silently catches error in systemd.daemon.notify
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* io: add option to retry first ident request
|
||||||
|
* config: fix merge_modules
|
||||||
|
* io: followup fix for retry-first-ident
|
||||||
|
* entangle: fix tango guards for pytango 9.3
|
||||||
|
|
||||||
|
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 13 Jun 2023 06:51:27 +0200
|
||||||
|
|
||||||
frappy-core (0.17.11) focal; urgency=medium
|
frappy-core (0.17.11) focal; urgency=medium
|
||||||
|
|
||||||
[ Alexander Zaft ]
|
[ Alexander Zaft ]
|
||||||
|
@ -401,10 +401,15 @@ class UniqueObject:
|
|||||||
def merge_status(*args):
|
def merge_status(*args):
|
||||||
"""merge status
|
"""merge status
|
||||||
|
|
||||||
the status with biggest code wins
|
for combining stati of different mixins
|
||||||
texts matching maximal code are joined with ', '
|
- the status with biggest code wins
|
||||||
|
- texts matching maximal code are joined with ', '
|
||||||
|
- if texts already contain ', ', it is considered as composed by
|
||||||
|
individual texts and duplication is avoided. when commas are used
|
||||||
|
for other purposes, the behaviour might be surprising
|
||||||
"""
|
"""
|
||||||
maxcode = max(a[0] for a in args)
|
maxcode = max(a[0] for a in args)
|
||||||
merged = [a[1] for a in args if a[0] == maxcode and a[1]]
|
merged = [a[1] for a in args if a[0] == maxcode and a[1]]
|
||||||
|
# use dict instead of set for preserving order
|
||||||
merged = {m: True for mm in merged for m in mm.split(', ')}
|
merged = {m: True for mm in merged for m in mm.split(', ')}
|
||||||
return maxcode, ', '.join(merged)
|
return maxcode, ', '.join(merged)
|
||||||
|
@ -640,6 +640,13 @@ class Module(HasAccessibles):
|
|||||||
all parameters are polled once
|
all parameters are polled once
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
"""called when the sever shuts down
|
||||||
|
|
||||||
|
any cleanup-work should be performed here, like closing threads and
|
||||||
|
saving data.
|
||||||
|
"""
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
"""polls important parameters like value and status
|
"""polls important parameters like value and status
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
@ -24,6 +23,7 @@
|
|||||||
"""Define helpers"""
|
"""Define helpers"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
@ -33,7 +33,6 @@ from frappy.dynamic import Pinata
|
|||||||
from frappy.lib import formatException, generalConfig, get_class, mkthread
|
from frappy.lib import formatException, generalConfig, get_class, mkthread
|
||||||
from frappy.lib.multievent import MultiEvent
|
from frappy.lib.multievent import MultiEvent
|
||||||
from frappy.params import PREDEFINED_ACCESSIBLES
|
from frappy.params import PREDEFINED_ACCESSIBLES
|
||||||
from frappy.modules import Attached
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from daemon import DaemonContext
|
from daemon import DaemonContext
|
||||||
@ -106,6 +105,12 @@ class Server:
|
|||||||
|
|
||||||
self._cfgfiles = cfgfiles
|
self._cfgfiles = cfgfiles
|
||||||
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
||||||
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||||
|
|
||||||
|
def signal_handler(self, _num, _frame):
|
||||||
|
if hasattr(self, 'interface') and self.interface:
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if not DaemonContext:
|
if not DaemonContext:
|
||||||
@ -127,17 +132,18 @@ class Server:
|
|||||||
return f"{cls.__name__} class don't know how to handle option(s): {', '.join(options)}"
|
return f"{cls.__name__} class don't know how to handle option(s): {', '.join(options)}"
|
||||||
|
|
||||||
def restart_hook(self):
|
def restart_hook(self):
|
||||||
pass
|
"""Actions to be done on restart. May be overridden by a subclass."""
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
global systemd # pylint: disable=global-statement
|
||||||
while self._restart:
|
while self._restart:
|
||||||
self._restart = False
|
self._restart = False
|
||||||
try:
|
try:
|
||||||
# TODO: make systemd notifications configurable
|
# TODO: make systemd notifications configurable
|
||||||
if systemd: # pylint: disable=used-before-assignment
|
if systemd:
|
||||||
systemd.daemon.notify("STATUS=initializing")
|
systemd.daemon.notify("STATUS=initializing")
|
||||||
except Exception:
|
except Exception:
|
||||||
systemd = None # pylint: disable=redefined-outer-name
|
systemd = None
|
||||||
try:
|
try:
|
||||||
self._processCfg()
|
self._processCfg()
|
||||||
if self._testonly:
|
if self._testonly:
|
||||||
@ -156,12 +162,26 @@ class Server:
|
|||||||
self.log.info('startup done, handling transport messages')
|
self.log.info('startup done, handling transport messages')
|
||||||
if systemd:
|
if systemd:
|
||||||
systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
|
systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
|
||||||
self.interface.serve_forever()
|
t = mkthread(self.interface.serve_forever)
|
||||||
self.interface.server_close()
|
# we wait here on the thread finishing, which means we got a
|
||||||
|
# signal to shut down or an exception was raised
|
||||||
|
# TODO: get the exception (and re-raise?)
|
||||||
|
t.join()
|
||||||
|
self.interface = None # fine due to the semantics of 'with'
|
||||||
|
# server_close() called by 'with'
|
||||||
|
|
||||||
|
self.log.info(f'stopped listenning, cleaning up'
|
||||||
|
f' {len(self.modules)} modules')
|
||||||
|
# if systemd:
|
||||||
|
# if self._restart:
|
||||||
|
# systemd.daemon.notify('RELOADING=1')
|
||||||
|
# else:
|
||||||
|
# systemd.daemon.notify('STOPPING=1')
|
||||||
|
for name in self._getSortedModules():
|
||||||
|
self.modules[name].shutdownModule()
|
||||||
if self._restart:
|
if self._restart:
|
||||||
self.restart_hook()
|
self.restart_hook()
|
||||||
self.log.info('restart')
|
self.log.info('restarting')
|
||||||
else:
|
|
||||||
self.log.info('shut down')
|
self.log.info('shut down')
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
@ -268,3 +288,41 @@ class Server:
|
|||||||
# history_path = os.environ.get('ALTERNATIVE_HISTORY')
|
# history_path = os.environ.get('ALTERNATIVE_HISTORY')
|
||||||
# if history_path:
|
# if history_path:
|
||||||
# from frappy_<xx>.historywriter import ... etc.
|
# from frappy_<xx>.historywriter import ... etc.
|
||||||
|
|
||||||
|
def _getSortedModules(self):
|
||||||
|
"""Sort modules topologically by inverse dependency.
|
||||||
|
|
||||||
|
Example: if there is an IO device A and module B depends on it, then
|
||||||
|
the result will be [B, A].
|
||||||
|
Right now, if the dependency graph is not a DAG, we give up and return
|
||||||
|
the unvisited nodes to be dismantled at the end.
|
||||||
|
Taken from Introduction to Algorithms [CLRS].
|
||||||
|
"""
|
||||||
|
def go(name):
|
||||||
|
if name in done: # visiting a node
|
||||||
|
return True
|
||||||
|
if name in visited:
|
||||||
|
visited.add(name)
|
||||||
|
return False # cycle in dependencies -> fail
|
||||||
|
visited.add(name)
|
||||||
|
if name in unmarked:
|
||||||
|
unmarked.remove(name)
|
||||||
|
for module in self.modules[name].attachedModules.values():
|
||||||
|
res = go(module.name)
|
||||||
|
if not res:
|
||||||
|
return False
|
||||||
|
visited.remove(name)
|
||||||
|
done.add(name)
|
||||||
|
l.append(name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
unmarked = set(self.modules.keys()) # unvisited nodes
|
||||||
|
visited = set() # visited in DFS, but not completed
|
||||||
|
done = set()
|
||||||
|
l = [] # list of sorted modules
|
||||||
|
|
||||||
|
while unmarked:
|
||||||
|
if not go(unmarked.pop()):
|
||||||
|
self.log.error('cyclical dependency between modules!')
|
||||||
|
return l[::-1] + list(visited) + list(unmarked)
|
||||||
|
return l[::-1]
|
||||||
|
@ -354,7 +354,7 @@ class Cryostat(CryoBase):
|
|||||||
timestamp = t
|
timestamp = t
|
||||||
self.read_value()
|
self.read_value()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdownModule(self):
|
||||||
# should be called from server when the server is stopped
|
# should be called from server when the server is stopped
|
||||||
self._stopflag = True
|
self._stopflag = True
|
||||||
if self._thread and self._thread.is_alive():
|
if self._thread and self._thread.is_alive():
|
||||||
|
234
frappy_mlz/seop.py
Normal file
234
frappy_mlz/seop.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Georg Brandl <g.brandl@fz-juelich.de>
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
"""Adapter to the existing SEOP 3He spin filter system daemon."""
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# eventually he3control
|
||||||
|
from he3d import he3cell # pylint: disable=import-error
|
||||||
|
|
||||||
|
from frappy.core import Attached
|
||||||
|
from frappy.datatypes import ArrayOf, FloatRange, IntRange, StatusType, \
|
||||||
|
StringType, TupleOf
|
||||||
|
from frappy.errors import CommandRunningError
|
||||||
|
from frappy.modules import Command, Drivable, Module, Parameter, Property, \
|
||||||
|
Readable
|
||||||
|
from frappy.rwhandler import CommonReadHandler
|
||||||
|
|
||||||
|
integral = IntRange()
|
||||||
|
floating = FloatRange()
|
||||||
|
string = StringType()
|
||||||
|
|
||||||
|
# Configuration is kept in YAML files to stay compatible to the
|
||||||
|
# traditional 3He daemon, for now.
|
||||||
|
|
||||||
|
|
||||||
|
class Cell(Module):
|
||||||
|
""" Dummy module for creating He3Cell object in order for other modules to talk to the hardware.
|
||||||
|
Only deals with the config, and rotating the paramlog.
|
||||||
|
"""
|
||||||
|
config_directory = Property(
|
||||||
|
'Directory for the YAML config files', datatype=string)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
self.cell = he3cell.He3_cell(
|
||||||
|
path.join(self.config_directory, 'cell.yml'))
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command(result=string)
|
||||||
|
def raw_config_file(self):
|
||||||
|
"""return unparsed contents of yaml file"""
|
||||||
|
with open(self.cell._He3_cell__cfg_filename, 'r', encoding='utf-8') as f:
|
||||||
|
return str(f.read())
|
||||||
|
|
||||||
|
@Command(string, result=string)
|
||||||
|
def cfg_get(self, identifier):
|
||||||
|
"""Get a configuration value."""
|
||||||
|
return str(self.cell.cfg_get(identifier))
|
||||||
|
|
||||||
|
@Command((string, string), result=string)
|
||||||
|
def cfg_set(self, identifier, value):
|
||||||
|
"""Set a configuration value."""
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# The type is lost during transmission.
|
||||||
|
# Check type so the value to be set has the same type and
|
||||||
|
# is not eg. a string where an int would be needed in the config key.
|
||||||
|
oldty = type(self.cell.cfg_get(identifier))
|
||||||
|
if oldty is not type(value):
|
||||||
|
raise ValueError('Type of value to be set does not match the '
|
||||||
|
'value in the configuration!')
|
||||||
|
return str(self.cell.cfg_set(identifier, value))
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def nmr_paramlog_rotate(self):
|
||||||
|
"""resets fitting and switches to a new logfile"""
|
||||||
|
self.cell.nmr_paramlog_rotate()
|
||||||
|
|
||||||
|
|
||||||
|
class Afp(Readable):
|
||||||
|
"""Polarisation state of the SEOP waveplates"""
|
||||||
|
|
||||||
|
value = Parameter('Current polarisation state of the SEOP waveplates', IntRange(0, 1))
|
||||||
|
|
||||||
|
cell = Attached(Cell)
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.cell.cell.afp_state_get()
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command(description='Flip polarization of SEOP waveplates')
|
||||||
|
def afp_flip(self):
|
||||||
|
self.cell.cell.afp_flip_do()
|
||||||
|
self.read_value()
|
||||||
|
|
||||||
|
|
||||||
|
class Nmr(Readable):
|
||||||
|
Status = Drivable.Status
|
||||||
|
status = Parameter(datatype=StatusType(Drivable.Status))
|
||||||
|
value = Parameter('Timestamp of last NMR', string)
|
||||||
|
cell = Attached(Cell)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
self.interval = 0
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return str(self.cell.cell.nmr_timestamp_get())
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
cellstate = self.cell.cell.nmr_state_get()
|
||||||
|
|
||||||
|
if self.cell.cell.nmr_background_check():
|
||||||
|
status = self.Status.BUSY, 'running every %d seconds' % self.interval
|
||||||
|
else:
|
||||||
|
status = self.Status.IDLE, 'not running'
|
||||||
|
|
||||||
|
# TODO: what do we do here with None and -1?
|
||||||
|
# -> None basically indicates that the fit for the parameters did not converge
|
||||||
|
if cellstate is None:
|
||||||
|
return self.Status.IDLE, f'returned None, {status[1]}'
|
||||||
|
if cellstate in (0, 1):
|
||||||
|
return status[0], f'nmr cellstate {cellstate}, {status[1]}'
|
||||||
|
if cellstate == -1:
|
||||||
|
return self.Status.WARN, f'got error from cell, {status[1]}'
|
||||||
|
return self.Status.ERROR, 'Unrecognized cellstate!'
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command()
|
||||||
|
def nmr_do(self):
|
||||||
|
"""Triggers the NMR to run"""
|
||||||
|
self.cell.cell.nmr_do()
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def bgstart(self):
|
||||||
|
"""Start background NMR"""
|
||||||
|
if self.isBusy():
|
||||||
|
raise CommandRunningError('backgroundNMR is already running')
|
||||||
|
interval = self.cell.cell.cfg_get('tasks/nmr/background/interval')
|
||||||
|
self.interval = interval
|
||||||
|
self.cell.cell.nmr_background_start(interval)
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def bgstop(self):
|
||||||
|
"""Stop background NMR"""
|
||||||
|
self.cell.cell.nmr_background_stop()
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
# Commands to get large datasets we do not want directly in the NICOS cache
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_processed_nmr(self):
|
||||||
|
"""Get data for processed signal."""
|
||||||
|
val= self.cell.cell.nmr_processed_get()
|
||||||
|
return (val['xval'], val['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_raw_nmr(self):
|
||||||
|
"""Get raw signal data."""
|
||||||
|
val = self.cell.cell.nmr_raw_get()
|
||||||
|
return (val['xval'], val['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_raw_spectrum(self):
|
||||||
|
"""Get the raw spectrum."""
|
||||||
|
val = self.cell.cell.nmr_raw_spectrum_get()
|
||||||
|
y = val['yval'][:len(val['xval'])]
|
||||||
|
return (val['xval'], y)
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_processed_spectrum(self):
|
||||||
|
"""Get the processed spectrum."""
|
||||||
|
val = self.cell.cell.nmr_processed_spectrum_get()
|
||||||
|
x = val['xval'][:len(val['yval'])]
|
||||||
|
return (x, val['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||||
|
ArrayOf(floating, maxlen=100)))
|
||||||
|
def get_amplitude(self):
|
||||||
|
"""Last 20 amplitude datapoints."""
|
||||||
|
rv = self.cell.cell.nmr_paramlog_get('amplitude', 20)
|
||||||
|
x = [ str(timestamp) for timestamp in rv['xval']]
|
||||||
|
return (x,rv['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||||
|
ArrayOf(floating, maxlen=100)))
|
||||||
|
def get_phase(self):
|
||||||
|
"""Last 20 phase datapoints."""
|
||||||
|
val = self.cell.cell.nmr_paramlog_get('phase', 20)
|
||||||
|
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
||||||
|
|
||||||
|
|
||||||
|
class FitParam(Readable):
|
||||||
|
value = Parameter('fitted value', unit='$', default=0.0)
|
||||||
|
sigma = Parameter('variance of the fitted value', FloatRange(), default=0.0)
|
||||||
|
param = Property('the parameter that should be accesssed',
|
||||||
|
StringType(), export=False)
|
||||||
|
|
||||||
|
cell = Attached(Cell)
|
||||||
|
|
||||||
|
@CommonReadHandler(['value', 'sigma'])
|
||||||
|
def read_amplitude(self):
|
||||||
|
ret = self.cell.cell.nmr_param_get(self.param)
|
||||||
|
self.value = ret['value']
|
||||||
|
self.sigma = ret['sigma']
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command(integral, result=TupleOf(ArrayOf(string),
|
||||||
|
ArrayOf(floating)))
|
||||||
|
def nmr_paramlog_get(self, n):
|
||||||
|
"""returns the log of the last 'n' values for this parameter"""
|
||||||
|
val = self.cell.cell.nmr_paramlog_get(self.param, n)
|
||||||
|
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
141
test/test_config.py
Normal file
141
test/test_config.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
# false positive with fixtures
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from frappy.config import Collector, Config, Mod, NodeCollector, load_config, \
|
||||||
|
process_file, to_config_path
|
||||||
|
from frappy.errors import ConfigError
|
||||||
|
from frappy.lib import generalConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerStub:
|
||||||
|
def debug(self, fmt, *args):
|
||||||
|
pass
|
||||||
|
info = warning = exception = error = debug
|
||||||
|
handlers = []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def log():
|
||||||
|
return LoggerStub()
|
||||||
|
|
||||||
|
|
||||||
|
PY_FILE = """Node('foonode', 'fodesc', 'fooface')
|
||||||
|
Mod('foo', 'frappy.modules.Readable', 'description', value=5)
|
||||||
|
Mod('bar', 'frappy.modules.Readable', 'about me', export=False)
|
||||||
|
Mod('baz', 'frappy.modules.Readable', 'things', value=Param(3, unit='BAR'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# fixture file system, TODO: make a bit nicer?
|
||||||
|
@pytest.fixture
|
||||||
|
def direc(tmp_path_factory):
|
||||||
|
d = tmp_path_factory.mktemp('cfgdir')
|
||||||
|
a = d / 'a'
|
||||||
|
b = d / 'b'
|
||||||
|
a.mkdir()
|
||||||
|
b.mkdir()
|
||||||
|
f = a / 'config_cfg.py'
|
||||||
|
pyfile = a / 'pyfile_cfg.py'
|
||||||
|
ff = b / 'test_cfg.py'
|
||||||
|
fff = b / 'alsoworks.py'
|
||||||
|
f.touch()
|
||||||
|
ff.touch()
|
||||||
|
fff.touch()
|
||||||
|
pyfile.write_text(PY_FILE)
|
||||||
|
generalConfig.testinit(confdir=f'{a}:{b}', piddir=str(d))
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
files = [('config', 'a/config_cfg.py'),
|
||||||
|
('config_cfg', 'a/config_cfg.py'),
|
||||||
|
('config_cfg.py', 'a/config_cfg.py'),
|
||||||
|
('test', 'b/test_cfg.py'),
|
||||||
|
('test_cfg', 'b/test_cfg.py'),
|
||||||
|
('test_cfg.py', 'b/test_cfg.py'),
|
||||||
|
('alsoworks', 'b/alsoworks.py'),
|
||||||
|
('alsoworks.py', 'b/alsoworks.py'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('file, res', files)
|
||||||
|
def test_to_cfg_path(log, direc, file, res):
|
||||||
|
assert to_config_path(file, log).endswith(res)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cfg_not_existing(direc, log):
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
to_config_path('idonotexist', log)
|
||||||
|
|
||||||
|
|
||||||
|
def collector_helper(node, mods):
|
||||||
|
n = NodeCollector()
|
||||||
|
n.add(*node)
|
||||||
|
m = Collector(Mod)
|
||||||
|
m.list = [Mod(module, '', '') for module in mods]
|
||||||
|
return n, m
|
||||||
|
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
(['n1', 'desc', 'iface'], ['foo', 'bar', 'baz'], ['n2', 'foo', 'bar'],
|
||||||
|
['foo', 'more', 'other'], ['n1', 'iface', 5, {'foo'}]),
|
||||||
|
(['n1', 'desc', 'iface'], ['foo', 'bar', 'baz'], ['n2', 'foo', 'bar'],
|
||||||
|
['different', 'more', 'other'], ['n1', 'iface', 6, set()]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('n1, m1, n2, m2, res', configs)
|
||||||
|
def test_merge(n1, m1, n2, m2, res):
|
||||||
|
name, iface, num_mods, ambig = res
|
||||||
|
c1 = Config(*collector_helper(n1, m1))
|
||||||
|
c2 = Config(*collector_helper(n2, m2))
|
||||||
|
c1.merge_modules(c2)
|
||||||
|
assert c1['node']['equipment_id'] == name
|
||||||
|
assert c1['node']['interface'] == iface
|
||||||
|
assert len(c1.module_names) == num_mods
|
||||||
|
assert c1.ambiguous == ambig
|
||||||
|
|
||||||
|
|
||||||
|
def do_asserts(ret):
|
||||||
|
assert len(ret.module_names) == 3
|
||||||
|
assert set(ret.module_names) == set(['foo', 'bar', 'baz'])
|
||||||
|
assert ret['node']['equipment_id'] == 'foonode'
|
||||||
|
assert ret['node']['interface'] == 'fooface'
|
||||||
|
assert ret['foo'] == {'cls': 'frappy.modules.Readable',
|
||||||
|
'description': 'description', 'value': {'value': 5}}
|
||||||
|
assert ret['bar'] == {'cls': 'frappy.modules.Readable',
|
||||||
|
'description': 'about me', 'export': {'value': False}}
|
||||||
|
assert ret['baz'] == {'cls': 'frappy.modules.Readable',
|
||||||
|
'description': 'things',
|
||||||
|
'value': {'value': 3, 'unit': 'BAR'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_file(direc, log):
|
||||||
|
ret = process_file(str(direc / 'a' / 'pyfile_cfg.py'), log)
|
||||||
|
do_asserts(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full(direc, log):
|
||||||
|
ret = load_config('pyfile_cfg.py', log)
|
||||||
|
do_asserts(ret)
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from frappy.lib import parse_host_port
|
from frappy.lib import parse_host_port, merge_status
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('hostport, defaultport, result', [
|
@pytest.mark.parametrize('hostport, defaultport, result', [
|
||||||
@ -46,3 +46,19 @@ def test_parse_host(hostport, defaultport, result):
|
|||||||
parse_host_port(hostport, defaultport)
|
parse_host_port(hostport, defaultport)
|
||||||
else:
|
else:
|
||||||
assert result == parse_host_port(hostport, defaultport)
|
assert result == parse_host_port(hostport, defaultport)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('args, result', [
|
||||||
|
([(100, 'idle'), (200, 'warning')],
|
||||||
|
(200, 'warning')),
|
||||||
|
([(300, 'ramping'), (300, 'within tolerance')],
|
||||||
|
(300, 'ramping, within tolerance')),
|
||||||
|
([(300, 'ramping, within tolerance'), (300, 'within tolerance, slow'), (200, 'warning')],
|
||||||
|
(300, 'ramping, within tolerance, slow')),
|
||||||
|
# when a comma is used for other purposes than separating individual status texts,
|
||||||
|
# the behaviour might not be as desired. However, this case is somewhat constructed.
|
||||||
|
([(100, 'blue, yellow is my favorite'), (100, 'white, blue, red is a bad color mix')],
|
||||||
|
(100, 'blue, yellow is my favorite, white, red is a bad color mix')),
|
||||||
|
])
|
||||||
|
def test_merge_status(args, result):
|
||||||
|
assert merge_status(*args) == result
|
||||||
|
55
test/test_server.py
Normal file
55
test/test_server.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
|
||||||
|
from frappy.server import Server
|
||||||
|
|
||||||
|
from .test_config import direc # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerStub:
|
||||||
|
def debug(self, fmt, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getChild(self, *args):
|
||||||
|
return self
|
||||||
|
|
||||||
|
info = warning = exception = error = debug
|
||||||
|
handlers = []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def log():
|
||||||
|
return LoggerStub()
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_only(direc, log):
|
||||||
|
"""only see that this does not throw. get config from name."""
|
||||||
|
s = Server('pyfile', log)
|
||||||
|
s._processCfg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_file(direc, log):
|
||||||
|
"""only see that this does not throw. get config from cfgfiles."""
|
||||||
|
s = Server('foo', log, cfgfiles='pyfile_cfg.py')
|
||||||
|
s._processCfg()
|
Loading…
x
Reference in New Issue
Block a user